Skip to content

0xdevelop/api_show

Repository files navigation

api_show — LAN-internal OpenAPI aggregation portal

English · 中文

Release Go License

A fast, lightweight, security- and privacy-first API documentation portal.

Caution

Applicability — read first. api_show is built for high-confidentiality, coarse-grained-permission environments:

  • Fits: internal / LAN-only API documentation that should never reach the public internet, where access control is expressed at project + tag-prefix granularity (e.g. "Alice can see project payments, tags auth* and users*").
  • Does NOT fit: SaaS-style multi-tenant docs portals, per-endpoint / per-field ACL, per-operation rate limiting, audit logging requirements, or any setup that needs to be exposed to the open internet.

If you need fine-grained operation-level permissions, audit trails, SSO/OAuth federation, or public hosting — pick a different tool. api_show deliberately trades feature breadth for a small, auditable surface that fits inside a LAN.

api_show aggregates OpenAPI specs from multiple producer repos into a single portal rendered by an embedded Redoc bundle (served locally — no CDN dependency). Each producer repo posts its docs/openapi.yaml on tag release; viewers open the same URL, pick project + version from a dropdown, and see the rendered API. Spec updates do not require recompiling the portal.

Screenshots

Sign in Bind TOTP (one-time)
Sign in Bind TOTP
Portal — demo_a Portal — demo_b
demo_a demo_b

The QR code and base32 secret in the bind screenshot are intentionally blurred / redacted — each first-time login generates a fresh secret.

Dark theme tokens match Redoc's left sidebar + right code panel. Schema labels and table rows lift contrast via webroot/style.css overrides scoped to #api-reference.

Threat model — LAN only. No public IP, never exposed to the internet. When only_local = true (default) any non-private / non-loopback IP hitting /upload is rejected at the application layer regardless of the Bearer token.

Auth — three modes, picked via the auth_mode field in TOML config:

Mode Behaviour
pwd username + password only. No TOTP. Best for IDE breakpoint debugging.
pwd_totp Default. Verify password; if the user has TOTP bound, require a 6-digit code as a 2nd factor. Otherwise issue a session straight away.
totp No password. TOTP is the credential; first login auto-enrols via /bind-totp.

Passwords are never stored in TOML. The argon2id hash lives in an AES-GCM-encrypted sidecar blob under <data_dir>/pwd_blobs/. The admin CLI lets you create users without ever knowing their password — see Adding a user below.


How it compares

api_show sits in a narrow niche: multi-spec aggregation behind a firewall, with per-user spec rewriting, no SaaS dependency, single-binary deploy. Most adjacent tools either drop one of those properties or are full SaaS platforms.

Legend: ✅ supported · ⚠️ partial / via plugin · ❌ not supported · — not applicable.

Capability api_show Swagger UI / Redoc (raw) Redocly CLI Stoplight Elements Stoplight Platform / Bump.sh ReadMe.com Mintlify Backstage TechDocs
Aggregate multiple OpenAPI specs in one portal ⚠️ ⚠️
Self-hosted single binary, no runtime deps ⚠️ (static files) ⚠️ ❌ (SaaS) ❌ (SaaS) ❌ (SaaS) ❌ (heavy platform)
LAN-only enforced at app layer (private/loopback IP gate) ⚠️
Built-in multi-user auth (pwd / TOTP / 2FA) ⚠️ (via Backstage)
Per-user ACL at project + tag-prefix level ⚠️ (org-level) ⚠️ ⚠️
Server-side spec rewrite drops disallowed ops before delivery
Producer upload endpoint (CI POSTs spec on tag) ⚠️
No CDN / no external font fetch at runtime ⚠️ ⚠️ ⚠️ ⚠️
Passwords never stored in config, argon2id PHC in AES-GCM blob ⚠️
Admin CLI never sees plaintext password (/set-password flow) ⚠️ ⚠️ ⚠️ ⚠️
Spec updates without recompiling / redeploying the portal ⚠️ ❌ (rebuild) ⚠️ ❌ (rebuild) ❌ (rebuild)
Audit log / SSO / fine-grained per-endpoint ACL ⚠️
Public-internet hosting is a supported deployment ❌ (intentional)

Pick api_show when all of these are true:

  • Multiple internal teams ship OpenAPI specs and you want one portal listing them.
  • Access stays inside a LAN / VPN; you do not want to depend on a SaaS or expose docs to the public internet.
  • Coarse access control (project + tag-prefix per user) is enough; you do not need per-endpoint or per-field ACL or audit trails.
  • You want a single Go binary with embedded Redoc — no Node toolchain, no build pipeline, no CDN call at runtime.

Pick something else when you need any of: per-endpoint ACL, audit logging, SSO/OIDC federation, public hosting, fine-grained quota / rate limiting, or a polished docs CMS with custom marketing pages.


Install (LAN deployment)

# 1. Download a release archive
wget https://github.com/0xYeah/api_show/releases/download/<VERSION>/api_show_linux_release_<VERSION>.zip
unzip api_show_linux_release_<VERSION>.zip && cd api_show

# 2. One-shot install (creates /api_show/{api_show, conf/, logs/}, installs unit file)
sudo ./install_api_show.sh

# 3. Start → edit config → restart
sudo systemctl start api_show
sudo $EDITOR /api_show/conf/api_show_config.toml   # fill secret + upload_token
sudo systemctl restart api_show
sudo systemctl status api_show

install_api_show.sh actions:

Action Behaviour
install (default) First install. Existing binary + config get a _YYYY-MM-DD_HH-MM-SS backup suffix.
update Stop service → replace binary (preserve data/ + conf/) → restart.
uninstall Stop service + delete unit file + delete /api_show/ directory (irreversible).

Configuration

Path: /api_show/conf/api_show_config.toml (auto-created on first start with placeholder values).

# HMAC cookie key + per-blob key derivation salt. >= 16 bytes, ASCII.
secret = "REPLACE_ME_at_least_16_chars"

# Bearer token /upload accepts.
upload_token = "REPLACE_ME_bearer_token"

# Root dir for uploaded openapi specs + totp_blobs/ + pwd_blobs/.
data_dir = "./data"

# HTTP listen port. Bound to IPv4 0.0.0.0 only.
port = 12110

# LAN-only mode. true = /upload rejects any non-private / non-loopback IP
# at the app layer, independent of Bearer correctness.
only_local = true

# Auth policy: pwd | pwd_totp (default) | totp
auth_mode = "pwd_totp"

# User roster + per-user ACL. Edit + restart to add/remove users.
# TOML never holds passwords — they live in encrypted blobs under
# <data_dir>/pwd_blobs/. See "Adding a user".
[[users]]
username = "alice"
acl = { demo_a = ["*"], demo_b = ["auth", "users"] }

[[users]]
username = "bob"
acl = { demo_a = ["*"] }

Constraints:

  • secret empty or under 16 bytes → process refuses to start.
  • upload_token empty → /upload returns 503 uploads disabled.
  • auth_mode not in {pwd, pwd_totp, totp} → process refuses to start.
  • No environment variables are read. Config is only sourced from TOML; deploy by overwriting the file.
  • The first start writes a placeholder config when the file is missing, so startup never fails because of a missing config file.

Login flows

pwd mode

  1. GET /login → enter username + password → submit.
  2. Argon2id verifies → 7-day session cookie issued → 302 to /.

pwd_totp mode (default)

  1. GET /login → enter username + password → submit.
  2. Password verified → portal checks whether the user has a bound TOTP.
    • Not bound → session issued directly → 302 to /.
    • Bound → 302 to /login-totp.
  3. /login-totp → enter the 6-digit code → session issued → 302 to /.

totp mode

  1. GET /login → enter username, leave TOTP blank → submit.
  2. Account not bound yet → 302 to /bind-totp, scan QR + enter first code → secret persisted (encrypted) → session issued → 302 to /.
  3. Subsequent logins: username + 6-digit code → verify → session → /.

First login when no password is set

If the user was added without -p, or reset_pwd was run against them, the portal redirects to /set-password on the next login attempt. The user picks a password (minimum 8 characters); the admin never sees plaintext.

Browsing the docs

After login, the root page lists allowed projects + versions in two dropdowns. The current selection is reflected in the URL, which can be shared with users who hold the same ACL.


Adding a user

The admin CLI never reveals plaintext passwords. Two add-user patterns are supported:

Pattern 1 — admin sets an initial password the user changes later:

sudo /api_show/api_show add_user alice -p "InitChangeMe!8" -acl "demo_a=*;demo_b=auth"
sudo systemctl restart api_show
# Communicate the initial password to Alice out-of-band (e.g. signed mail).
# She can change it any time the admin runs `reset_pwd alice` (next login → /set-password).

Pattern 2 — user picks their own password on first login:

sudo /api_show/api_show add_user alice -acl "demo_a=*"
sudo systemctl restart api_show
# Tell Alice to visit /login with her username + any password.
# She'll be redirected to /set-password to pick her own.

-acl syntax: proj1=tag1,tag2;proj2=*. Use * to grant every tag in a project. Leaving -acl off writes an empty ACL block (admin can edit [[users]] in the TOML afterwards).

The CLI appends a new [[users]] block to the live config file (preserving your comments) and, with -p, writes the encrypted password blob.


Maintenance commands

./api_show version                              # build identity (commit / time / OS / Go version)
./api_show add_user <user> [-p <plaintext>]     # append [[users]] block + optional pwd blob
                  [-acl <spec>]
./api_show reset_pwd  <user>                    # delete pwd blob; user re-sets via /set-password
./api_show reset_totp <user>                    # delete TOTP blob; user re-binds on next login (totp / pwd_totp modes)

Pushing specs (producer side)

The producer's release script POSTs docs/openapi.yaml to the portal only after git push --tags succeeds. Endpoint + token come from a git-ignored file in the producer repo (no environment variables):

# In the producer repo root
cat > .gen_docs.local <<'EOF'
DEPLOY_ADDR=http://<portal-host>:12110
DEPLOY_TOKEN=<must match api_show's upload_token>
# ALLOW_LOOPBACK=1   # uncomment ONLY for local self-test
EOF

Producers ship their own gen_docs.sh (or equivalent) in each repo. A reference implementation rejects loopback addresses (127.0.0.1, localhost, [::1], 0.0.0.0) unless ALLOW_LOOPBACK=1 is set for local self-test.

Manual upload (local self-test):

curl -X POST \
  -H "Authorization: Bearer <upload_token>" \
  --data-binary @./docs/openapi.yaml \
  "http://127.0.0.1:12110/upload?project=<proj_id>&version=v0.0.1&latest=true"
Param Meaning
project Project id (used as the data directory name and matched against ACL keys).
version vX.Y.Z form.
latest true marks this version as the default (selected first in the dropdown).

Permissions (ACL)

Roster + permissions live in TOML [[users]]:

  • Each acl map: project_id → allowed tag prefixes.
  • * = every tag in that project.
  • Project absent from the user's acl = invisible to that user.
  • GET /api/sources.json filters out projects the caller cannot see.
  • GET /data/<proj>/<ver>/openapi.yaml rewrites the spec to drop disallowed operations + tag declarations.

Roster changes = config edit + restart. Edit /api_show/conf/api_show_config.toml (or use add_user / reset_pwd) then systemctl restart api_show. No tag / release dance required.


Health check

curl http://<portal-host>:12110/healthz   # 200 OK = alive

Upgrade

sudo ./install_api_show.sh update

update keeps /api_show/data/ (specs + TOTP/pwd blobs) and /api_show/conf/; only the binary is replaced.


Uninstall

sudo ./install_api_show.sh uninstall

Stops the service, removes the unit file, and deletes /api_show/ recursively (including data — irreversible).


Security envelope

  • LAN-only deployment by design. only_local = true rejects external IPs inside /upload regardless of token.
  • Listener is bound to 0.0.0.0 (IPv4) — no IPv6 dual-stack listener.
  • secret (≥ 16 bytes) is used for HMAC-signed cookies and AES-GCM key derivation for both pwd and TOTP blobs (per-user keys).
  • Passwords: argon2id (m=64 MiB, t=3, p=2) with PHC-formatted output, never stored in plaintext or in TOML.
  • Cookies: HttpOnly + SameSite=Lax. Set Secure=true once you front the service with TLS (e.g. nginx).
  • No remote write APIs other than /upload; user roster mutations require filesystem-level access on the host.

Build from source

go build ./...           # debug binary
./build.sh               # release tarball with version stamp

go.mod requires Go 1.24+. Dependencies are pure Go.


License

See LICENSE.