Import the MetaMesh app family from Worph/AppStore

23 apps (MetaCore, the library/network/client tiers and 13 MetaFeeder*)
move out of the general-purpose Worph/AppStore into their own store so the
MetaMesh family releases on its own cadence.

- asset URLs repointed to cdn.jsdelivr.net/gh/Worph/MetaAppStore@main
- store scaffolding added (category/recommend/featured lists, licence,
  contributing, gitattributes)
- psd-source/meta-icons-v2 icon generator follows the apps

MetaMCP stays in Worph/AppStore: it is third-party metatool-ai software,
not part of MetaMesh.
This commit is contained in:
Worph
2026-09-13 16:18:29 +00:00
parent 998eba19f4
commit 39f82e1f96
145 changed files with 12964 additions and 1 deletions
+341
View File
@@ -0,0 +1,341 @@
name: metagateway
# =============================================================================
# MetaGateway (shared backend) — source-bridging tier for the MetaMesh network.
#
# This is the SHARED-BACKEND variant of MetaGateway. It ships only the gateway
# dispatcher plus the card feeder, and REUSES the standalone MetaCore and
# MetaShare apps already installed on the box:
#
# metagateway-app → the dispatcher / gateway UI
# metagateway-card → the card feeder (curated-row cards for MetaWatch's wall)
# metagateway → AppShield OIDC gate in front of the dispatcher
#
# Contrast with the `MetaGateway` app in this store, which is SELF-CONTAINED
# (its own metagateway-core + metagateway-share + bundled Prowlarr/Tribler).
# Pick exactly one: both use compose project `metagateway` and the same
# /DATA/AppData/metagateway paths, so they cannot be installed side by side.
#
# Deployed and validated on watch.nsl.sh (2026-08-04).
#
# REQUIRES, on the same box and on the `pcs` network:
# - MetaCore → provides metacore-app (record store the dispatcher writes to)
# - MetaShare → provides metashare-app (seeds the CIDs the gateway resolves)
# Indexer sources (torznab/Prowlarr and friends) are configured at runtime in
# gateway-config.json under /DATA/AppData/metagateway/state — not here. Install
# the separate **Prowlarr** and **MetaFeederProwlarr** apps to feed it.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metagateway-app — the gateway dispatcher. Resolves external-source queries
# into MetaMesh records and writes them to the standalone metacore-app, whose
# co-located metashare-app then seeds the resolved CIDs.
# ---------------------------------------------------------------------------
metagateway-app:
image: ghcr.io/worph/meta-gateway:1.0.25
container_name: metagateway-app
hostname: metagateway-app
restart: unless-stopped
user: "1000:1000"
cpu_shares: 40
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the dev sub-stack limits.
mem_limit: 1g
memswap_limit: 1g
expose:
- 3000
# Public libp2p listener. This is the peer a remote MetaWatch's meta-search
# discovers via the kad `metamesh-gateway` provider namespace and dials for
# live gateway fan-out. Must be host-published AND reachable inbound.
ports:
- "4002:4002"
volumes:
# gateway-config.json (source definitions, feeder URLs) + dispatcher state.
- ${DATA_ROOT:-/DATA}/AppData/metagateway/state:/data/meta-gateway:rw
environment:
# Advertised verbatim in this peer's libp2p identify `agent_version`
# (`baseUrl=...`) and used for NOTHING else in-process — see
# meta-gateway `main.rs`: "prod compose would set it to the public
# hostname behind whatever perimeter is in front."
#
# It MUST be resolvable BY A REMOTE PEER. It used to be
# `http://metagateway-app:3000`, a docker-internal name, which made the
# gateway useless off-host even once discovery worked: a remote
# meta-search reads the served file_types/content_kinds by plain HTTP
# `GET {baseUrl}/api/gateway/plugins` (the contract in the mirrored
# `gateway_discovery.rs`), and an empty capability snapshot means
# `upstreams_for_query_from_raw` fans out to NOBODY — every remote
# typed query silently returns zero gateway hits. The `/api/gateway/plugins`
# carve-out in the Caddy labels below is what makes this URL answerable.
META_GATEWAY_BASE_URL: "https://metagateway-${APP_DOMAIN}"
META_GATEWAY_PEER_ID: "metagateway"
# --- cross-node discoverability (added 2026-08-26) ---------------------
# WITHOUT META_GATEWAY_PUBLIC_ADDR meta-gateway starts in local/mDNS-only
# mode: it loads NO kad bootstraps, sets kad_provide_enabled=false, and so
# never publishes its `metamesh-gateway` provider record — no off-host
# MetaWatch can EVER discover it, however open the firewall is. It also
# binds an ephemeral /tcp/0, so the `ports` mapping above would have
# nothing to forward to. It says so in its own first three log lines:
# network_mode="local" kad_bootstrap_count=0 kad_provide_enabled=false
# Verify after install: /api/peers → network_mode "public" + a non-empty
# external_addrs. (This is the gap that made watch.nsl.sh invisible to the
# wisera MetaWatch until 2026-08-26 — the self-contained MetaGateway
# listing always had it; this shared-backend variant did not.)
#
# Use the IPV4 spelling, NOT the bare APP_PUBLIC_IP: on an IPv6-primary
# PCS that variable holds the v6 address, and `/ip4/<v6>/tcp/...` is not a
# parseable multiaddr — the gateway REFUSES TO START rather than announce
# a lie. watch.nsl.sh is exactly such a box.
# Ceiling on established libp2p connections (new in meta-gateway 1.0.25).
# Joining the public DHT is otherwise UNBOUNDED: at 1.0.24 this peer
# climbed to ~600 connections and was OOM-killed by the memory limit
# below roughly once a minute, and because each boot minted a fresh
# peer-id, every provider record it published was stale within seconds —
# remote peers only ever got "Unexpected peer ID". 128 is the default;
# raise it only together with the memory limit.
META_GATEWAY_MAX_CONNECTIONS: "128"
META_GATEWAY_P2P_LISTEN: "/ip4/0.0.0.0/tcp/4002"
META_GATEWAY_PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4002"
# Public IPFS DHT bootstraps by explicit /dns4/.../tcp/4001 (the nested
# dnsaddr default resolves to wss-only in some container resolvers).
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
# It is here for resilience: the four IPFS-Foundation bootstrappers can
# refuse an individual IP, and when they do a peer cannot join the DHT AT
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
# reachable fifth was added. Costs nothing when the four work.
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
TZ: "$TZ"
depends_on:
metagateway-card:
condition: service_healthy
required: false
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
labels:
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
networks:
- metagateway
- pcs
# ---------------------------------------------------------------------------
# metagateway-card — the CARD feeder. Answers the curated-row card queries
# that populate MetaWatch's home wall (one query per row, no per-title
# anchoring). Optional: the dispatcher starts without it, but the wall on
# every connected MetaWatch stays empty.
#
# Runs as 10001:999 — the uid/gid baked into the meta-feeder-tmdb image.
# ---------------------------------------------------------------------------
metagateway-card:
image: ghcr.io/worph/meta-feeder-tmdb:1.0.2
container_name: metagateway-card
hostname: metagateway-card
restart: unless-stopped
user: "10001:999"
cpu_shares: 30
mem_limit: 256m
memswap_limit: 256m
expose:
- 8080
volumes:
- ${DATA_ROOT:-/DATA}/AppData/metagateway/card:/data/meta-feeder:rw
environment:
RUST_LOG: "info"
TZ: "$TZ"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
labels:
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
networks:
- metagateway
# ---------------------------------------------------------------------------
# AppShield gate — the public entrypoint to the dispatcher UI, behind Yundera
# OIDC SSO. container_name MUST equal the subdomain prefix (`metagateway`):
# auth-registrar derives the OIDC client_id from it.
#
# /health is handled ahead of the gate so probes don't need a session.
# `pcs` is REQUIRED — the gate resolves auth-registrar and casaos-oidc-bridge
# by name on that network.
# ---------------------------------------------------------------------------
metagateway:
image: ghcr.io/yundera/appshield:2.0.9
container_name: metagateway
hostname: metagateway
restart: unless-stopped
user: "0:0"
cpu_shares: 90
expose:
- 80
environment:
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
BACKEND_HOST: "metagateway-app"
BACKEND_PORT: "3000"
LISTEN_PORT: "80"
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
depends_on:
- metagateway-app
labels:
# ---- metagateway-${APP_DOMAIN} (gateway TLS) ----
caddy_0: metagateway-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.0_handle: "/health"
caddy_0.0_handle.reverse_proxy: "metagateway-app:3000"
# Gateway capability contract, read by REMOTE meta-search/meta-share
# peers that have no session and never will. Exact-match path (no
# trailing `*`), so the mutating `PUT /api/gateway/plugins/:id` and
# every `/api/metadata/*` write stay behind AppShield.
# Response is capability-only: plugin ids + file_types/content_kinds
# + nzbFetch + nzbIndexers (indexer HOSTNAMES, no API keys).
caddy_0.1_handle: "/api/gateway/plugins"
caddy_0.1_handle.reverse_proxy: "metagateway-app:3000"
caddy_0.2_handle: ""
caddy_0.2_handle.reverse_proxy: "{{upstreams 80}}"
# ---- metagateway-IP.nip.io (gateway TLS) ----
caddy_1: metagateway-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.0_handle: "/health"
caddy_1.0_handle.reverse_proxy: "metagateway-app:3000"
# Gateway capability contract, read by REMOTE meta-search/meta-share
# peers that have no session and never will. Exact-match path (no
# trailing `*`), so the mutating `PUT /api/gateway/plugins/:id` and
# every `/api/metadata/*` write stay behind AppShield.
# Response is capability-only: plugin ids + file_types/content_kinds
# + nzbFetch + nzbIndexers (indexer HOSTNAMES, no API keys).
caddy_1.1_handle: "/api/gateway/plugins"
caddy_1.1_handle.reverse_proxy: "metagateway-app:3000"
caddy_1.2_handle: ""
caddy_1.2_handle.reverse_proxy: "{{upstreams 80}}"
# ---- metagateway-IP.sslip.io (Let's Encrypt) ----
caddy_2: metagateway-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.0_handle: "/health"
caddy_2.0_handle.reverse_proxy: "metagateway-app:3000"
# Gateway capability contract, read by REMOTE meta-search/meta-share
# peers that have no session and never will. Exact-match path (no
# trailing `*`), so the mutating `PUT /api/gateway/plugins/:id` and
# every `/api/metadata/*` write stay behind AppShield.
# Response is capability-only: plugin ids + file_types/content_kinds
# + nzbFetch + nzbIndexers (indexer HOSTNAMES, no API keys).
caddy_2.1_handle: "/api/gateway/plugins"
caddy_2.1_handle.reverse_proxy: "metagateway-app:3000"
caddy_2.2_handle: ""
caddy_2.2_handle.reverse_proxy: "{{upstreams 80}}"
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
networks:
- metagateway
- pcs
networks:
metagateway:
name: metagateway
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Media
developer: Aptero Team
hostname: metagateway-${APP_DOMAIN}
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
index: /
is_uncontrolled: false
main: metagateway
port_map: "443"
scheme: https
store_app_id: metagatewayshared
title:
en_us: MetaGateway (shared backend)
tagline:
en_us: MetaGateway reusing the standalone MetaCore + MetaShare apps
fr_fr: MetaGateway réutilisant les apps MetaCore et MetaShare autonomes
description:
en_us: |
**The source-bridging tier for the MetaMesh network.**
MetaGateway ingests content from external sources and makes it
discoverable across the MetaMesh federation — so MetaWatch (and other
MetaShare peers) can find and stream it.
This build is the **shared-backend** variant: it ships the dispatcher and
the card feeder only, and **requires** the standalone MetaCore and
MetaShare apps on the same box, which it reuses for the record store and
for seeding resolved CIDs. One core, one share — no duplicated services.
If you want a stack that bundles its own core, share, Prowlarr and
Tribler instead, install **MetaGateway**.
**How it fits:** MetaGateway is the *producer*; **MetaWatch** is the
*consumer*. Install MetaGateway to populate the network, then browse it in
MetaWatch.
**Sources** are configured at runtime in `gateway-config.json`; install
**Prowlarr** and **MetaFeederProwlarr** (and optionally
**MetaFeederUsenet**) to supply them.
fr_fr: |
**La couche de passerelle de sources pour le réseau MetaMesh.**
MetaGateway ingère du contenu depuis des sources externes et le rend
découvrable à travers la fédération MetaMesh — pour que MetaWatch (et les
autres pairs MetaShare) puissent le trouver et le lire.
Cette version est la variante **backend partagé** : elle n'embarque que le
dispatcher et le feeder de cartes, et **nécessite** les apps MetaCore et
MetaShare autonomes sur la même machine. Un seul cœur, un seul share —
aucun doublon.
**Place dans l'écosystème :** MetaGateway est le *producteur* ; MetaWatch
est le *consommateur*.
tips:
before_install:
en_us: |
**Requires MetaCore + MetaShare.** Install both standalone apps first —
this build reuses them instead of bundling its own copies.
**Conflicts with `MetaGateway`.** Both use the compose project
`metagateway` and the same data paths. Install one or the other.
**Sources are runtime config.** After install, define your sources in
`/DATA/AppData/metagateway/state/gateway-config.json` (or through the
dispatcher UI). Install **Prowlarr** + **MetaFeederProwlarr** to give it
something to query.
**The card feeder matters.** `metagateway-card` answers the curated-row
card queries that fill MetaWatch's home wall. Without it the dispatcher
still works for search, but every connected MetaWatch shows an empty
wall.
# Maison (Yundera dashboard) configuration. Read in preference to x-casaos.
# schema_version 2 is required for `folders`: an older Maison refuses the app
# outright rather than starting it without its directories.
x-compose-app:
schema_version: 2
# Created and chowned by Maison before any image is pulled, before any hook
# runs and before the containers start — on the first boot and every one after.
# Compose would otherwise create a missing bind source as a root-owned dir.
folders:
- path: /DATA/AppData/metagateway/state
user: $PUID
group: $PGID
- path: /DATA/AppData/metagateway/card
user: 10001
group: 999
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metagateway-${domain}
webui-path: "/"
+52
View File
@@ -0,0 +1,52 @@
# MetaGateway (shared backend) — Rationale
## What deviation / exception is being requested
1. `metagateway-card` runs as `10001:999` rather than `$PUID:$PGID`.
2. The stack reuses the **standalone MetaCore and MetaShare apps'** services over the shared
`pcs` network instead of bundling its own.
3. The compose top-level `name:` is `metagateway`, the same project id as the
self-contained `MetaGateway` app in this store.
## Why it is necessary
1. `10001:999` is the uid/gid the upstream meta-feeder-tmdb image runs its application as;
its writable paths are baked to that owner. `/DATA/AppData/metagateway/card` is declared
in `x-compose-app.folders` with `user: 10001` / `group: 999` so Maison creates it with
the right ownership before the first start, instead of Compose creating it root-owned and
the container failing on its first write.
2. This is the point of the variant: on a box that already runs MetaCore and MetaShare,
bundling a second Redis, leader-election tier and transport peer wastes memory and
splits the record store in two.
3. The two variants are alternatives, not companions — they share the project id and the
same `/DATA/AppData/metagateway/` paths precisely so that installing one over the other
reuses the same data rather than orphaning it. Exactly one may be installed at a time;
this is stated in `tips.before_install`.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`)
behind the PCS's Authelia SSO. The backends carry no Caddy labels and publish no host
port.
- No user directories are mounted — no `/DATA/Documents`, `/DATA/Downloads`, `/DATA/Media`
or `/DATA/Gallery`.
- All binds are under `/DATA/AppData/metagateway/`, declared in `x-compose-app.folders`.
- `cpu_shares` on every service; image tags pinned; no `:latest`.
## Alternatives considered and rejected
- **Run `metagateway-card` as `$PUID:$PGID`.** The upstream image's baked paths are owned by
`10001:999`; it fails on first write.
- **`chown` the directory in a hook instead.** The guidelines are explicit that a hook's
`mkdir`/`chown` runs in the Maison container against a host path — the wrong filesystem.
`folders` is the supported mechanism and it runs before the first pull.
- **A distinct project id, so both variants can coexist.** They would then fight over the
same public hostnames and the same record store; two gateways on one box is not a
supported topology.
## Data protection
State is under `/DATA/AppData/metagateway/`, the unit Maison archives on uninstall and
restores from backup. Because both MetaGateway variants use that path, switching between
them preserves the user's configuration. The MetaCore and MetaShare data this variant reads
belong to those apps and are archived with them.