Build / Build and release (push) Failing after 19s
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
66 lines
3.6 KiB
Markdown
66 lines
3.6 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:** https://13ft.poweredbyotm.com on **anvil**, hand-provisioned
|
||
from the STOCK image `wasimaster/13ft:latest` (0.3.4) via
|
||
`platform/infra/13ft/setup-13ft-anvil.sh` (platform#91, closed) — a
|
||
`/srv/otm/clients/13ft/` compose stack + Caddy site with **basic auth**
|
||
(user `tony`; 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`):
|
||
|
||
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.
|