Files
2026-10-18 23:18:34 -05:00

3.7 KiB
Raw Permalink Blame History

13ft — OTM's fork

A self-hosted "read the article" fetcher (Python/Flask + gunicorn, ~200 lines in app/ (portable.py is the entrypoint)): it refetches a URL as Googlebot and serves the page without the paywall/ad cruft. Forked 2026-10-14 from wasi-master/13ft (the original project was taken down once already — wasm-fyi → wasi-master — which is WHY we hold a fork, not a dependency on GitHub/Docker Hub).

  • Remotes: gitea = tonym/13ft on git.poweredbyotm.com (ours, push here); upstream = GitHub wasi-master/13ft (pull only: git fetch upstream && git merge upstream/main). Token for Gitea lives in ~/Projects/CLAUDE.md.
  • Running today (since 2026-10-19, v0.4.0): https://13ft.poweredbyotm.com on anvil as a FIRST-PARTY APP built from this repo (platform-13ft:latest, catalogue entry in otm-admin 0.148.0) behind Sign in with OTM — grant app:13ft on /users. The old stock-image + Caddy basic-auth stack (platform/infra/13ft/setup-13ft-anvil.sh, platform#91) is gone; the script is marked SUPERSEDED. Deploy = push → build_app 13ft → deploy_app 13ft.

Where this is going (the reason for the fork)

Make it a proper OTM first-party app per platform/docs/first-party-app-guide.md (house rules are CI-enforced by platform/test/first-party-house-rules.test.ts):

  1. Build from this repo — catalogue entry in platform/apps/otm-admin/lib/first-party-apps.ts (id: "13ft", repo tonym/13ft, image platform-13ft:latest, port 5000, hostname: 13ft.poweredbyotm.com), then build_app 13ft / deploy_app 13ft via the platform MCP. First: destroy or rename the hand-provisioned 13ft stack (same slug + hostname + container name) or the two will fight over the Caddy site and the container name.
  2. Sign in with OTM replaces Caddy basic auth: needsAuthSecret → OTM_SSO_SECRET; add /api/auth/otm-sso (verify the HS256 ticket for aud app:13ft — port pickup/src/sso.js, or in Python: HMAC-SHA256 over header.payload, base64url, check exp/aud/jti, single-use) and a session cookie; gate every fetch route on it. Grant-only (NOT publicAccess) — a stranger's OTM account must not get a free fetch proxy.
  3. tides (needsAnalytics) — the beacon <script defer src="${TIDES_HOST}/t.js" data-site="${TIDES_SITE_ID}"> in the page template. Ideas (needsSuggestions) — a lightbulb that files Gitea issues (see pickup/src/ideas.js for the plain-HTTP shape). Version + /changelog + /api/health reporting the version + footer "Powered by OTM".
  4. Keep the Dockerfile's non-root user (thirteener) and gunicorn; add a HEALTHCHECK.

Status 2026-10-18 (v0.4.0): steps 1–4 are built. app/otm.py holds the ticket verifier + session cookie + health/changelog/footer/Ideas; portable.py mounts the before_request gate (everything but otm.PUBLIC_PATHS needs a session; no secret = 503, never open) and the routes. Tests: cd app && ../.venv/bin/python -m unittest (pure test_otm.py + Flask test-client test_app.py). The platform catalogue entry landed in otm-admin 0.148.0. Deploy = stop the hand stack (cd /srv/otm/clients/13ft && docker compose down), then build_app 13ft + deploy_app 13ft; grant app:13ft to each reader on /users. The in-memory jti guard assumes gunicorn's single worker.

Dev

python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt
cd app && python portable.py   # http://127.0.0.1:5000 (the app lives in app/)
docker build -t 13ft-dev . && docker run --rm -p 5000:5000 13ft-dev

Upstream README (below in this repo's README.md) covers the rest. Keep upstream's MIT license file.