app/otm.py is the Python port of pickup's ticket verifier (HS256, mandatory exp, audience check, alg only read to reject, never raises) plus a signed session cookie and an in-memory single-use jti guard. portable.py gates every route but PUBLIC_PATHS behind a session and fails CLOSED (503) with no secret — it is a fetch proxy. /api/health reports the version, /changelog renders CHANGELOG.md, the footer carries version/sign-out/Ideas, tides beacon from TIDES_HOST/TIDES_SITE_ID. Dockerfile: OTM labels + HEALTHCHECK. 17 tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
3.6 KiB
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/13fton 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: https://13ft.poweredbyotm.com on anvil, hand-provisioned
from the STOCK image
wasimaster/13ft:latest(0.3.4) viaplatform/infra/13ft/setup-13ft-anvil.sh(platform#91, closed) — a/srv/otm/clients/13ft/compose stack + Caddy site with basic auth (usertony; password root-only in/srv/otm/clients/13ft/secrets/basic-auth.txt). Nothing in this repo is deployed yet.
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):
- Build from this repo — catalogue entry in
platform/apps/otm-admin/lib/first-party-apps.ts(id: "13ft", repotonym/13ft, imageplatform-13ft:latest, port 5000,hostname: 13ft.poweredbyotm.com), thenbuild_app 13ft/deploy_app 13ftvia the platform MCP. First: destroy or rename the hand-provisioned13ftstack (same slug + hostname + container name) or the two will fight over the Caddy site and the container name. - Sign in with OTM replaces Caddy basic auth:
needsAuthSecret→OTM_SSO_SECRET; add/api/auth/otm-sso(verify the HS256 ticket for audapp:13ft— portpickup/src/sso.js, or in Python: HMAC-SHA256 overheader.payload, base64url, checkexp/aud/jti, single-use) and a session cookie; gate every fetch route on it. Grant-only (NOTpublicAccess) — a stranger's OTM account must not get a free fetch proxy. - 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 (seepickup/src/ideas.jsfor the plain-HTTP shape). Version +/changelog+/api/healthreporting the version + footer "Powered by OTM". - Keep the Dockerfile's non-root user (
thirteener) and gunicorn; add aHEALTHCHECK.
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.