66 lines
3.7 KiB
Markdown
66 lines
3.7 KiB
Markdown
# 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](https://github.com/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
|
||
|
||
```sh
|
||
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.
|