From 45b72c242a699fb2c14af554a0c86d0b1b2ef80d Mon Sep 17 00:00:00 2001 From: Bennett Spooner Date: Mon, 21 Sep 2026 17:32:09 -0500 Subject: [PATCH] Docs: make the Railway steps require FOUNDER_OS_ACCESS_TOKEN The access gate shipped in #1 / f30a715, but the README section that documented it was lost in a later rewrite: the token appeared nowhere in README.md or .env.example, and the Railway steps still walked a first-time deployer onto a public URL with every route open (issue #2). - Railway steps now set the token before any credentials, and end with a check that the deployed URL shows the challenge page - New "Access control" section: no built-in auth, unset means open, how to generate and enter the token - .env.example lists FOUNDER_OS_ACCESS_TOKEN first - tests/access-gate.test.ts pins all three so the docs can't drift again Gate behaviour is unchanged (still opt-in, so local dev and the read-only demo stay open). Closes #2 Co-Authored-By: Claude Fable 5.1 --- .env.example | 6 ++++++ README.md | 35 +++++++++++++++++++++++++++++++++-- tests/access-gate.test.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 81904628..09178554 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,11 @@ # FOUNDER OS — copy to .env.local and fill in. .env.local is gitignored. +# ── Access gate ────────────────────────────────────────────────────────── +# REQUIRED on any public deployment (Railway etc.). The app has no auth of its +# own; unset = every page and API route is open. Generate: openssl rand -hex 32 +# Leave blank for local dev. +FOUNDER_OS_ACCESS_TOKEN= + # ── Email (up to 4 IMAP inboxes) ───────────────────────────────────────── # Gmail: use an App Password (Google Account → Security → 2FA → App passwords) INBOX_1_HOST=imap.gmail.com diff --git a/README.md b/README.md index 769c0348..38ca24f5 100644 --- a/README.md +++ b/README.md @@ -216,10 +216,41 @@ so they never touch the seeded dev DB. 1. Create a Railway project and point it at this repo. 2. Set the build command to `npm run build` and the start command to `npm start` (the app serves on the `PORT` Railway provides). -3. Add a database service and any connector credentials as environment variables +3. **Set `FOUNDER_OS_ACCESS_TOKEN` before adding any credentials.** Railway + serves the app on a public `*.up.railway.app` URL, and with the token unset + every page and API route is open to anyone who finds it. See + [Access control](#access-control). +4. Add a database service and any connector credentials as environment variables in the Railway dashboard. -4. Deploy. The knowledge services (G-Brain and Optimal Engine) run as companion +5. Deploy. The knowledge services (G-Brain and Optimal Engine) run as companion services and are referenced by URL from the app's environment. +6. Open the deployed URL in a private window and confirm you get the token + prompt, not the dashboard. + +--- + +## Access control + +Founder OS is a single-operator app: it has no user accounts and **no +authentication of its own**. On your laptop that is fine. On a public URL it +means anyone who finds the domain can read connector status, send replies as +you through `/api/comms/reply`, and run any agent through +`/api/agents/[id]/run`. + +The access gate closes that. Set one environment variable on the host: + +```bash +FOUNDER_OS_ACCESS_TOKEN=$(openssl rand -hex 32) +``` + +With it set, `middleware.ts` challenges every page and API route. Enter the +token once (the prompt, or `https://your-app/?token=…`) and an `httpOnly` cookie +remembers that browser for 30 days. Rotating the token signs every browser out. + +The gate is opt-in. **Unset means open**, which keeps local dev and the +read-only public demo working with no setup, so a deployment that holds real +credentials must set it. The decision logic lives in `lib/access-gate.ts`, with +its contract in `tests/access-gate.test.ts`. --- diff --git a/tests/access-gate.test.ts b/tests/access-gate.test.ts index cf3c123b..fe210601 100644 --- a/tests/access-gate.test.ts +++ b/tests/access-gate.test.ts @@ -62,3 +62,31 @@ describe('middleware wiring', () => { expect(src).toMatch(/matcher/); }); }); + +/** + * The gate is opt-in, so the docs are the only thing standing between a + * first-time deployer and a public URL with live keys behind it (issue #2). + * If the deploy steps stop naming the token, the gate may as well not exist. + */ +describe('deploy docs name the gate', () => { + const readme = readFileSync(join(process.cwd(), 'README.md'), 'utf8'); + const envExample = readFileSync(join(process.cwd(), '.env.example'), 'utf8'); + + test('the Railway steps require FOUNDER_OS_ACCESS_TOKEN', () => { + const start = readme.indexOf('## Deploying to Railway'); + expect(start).toBeGreaterThan(-1); + const next = readme.indexOf('\n## ', start + 1); + const railway = readme.slice(start, next === -1 ? undefined : next); + expect(railway).toContain('FOUNDER_OS_ACCESS_TOKEN'); + }); + + test('README has an Access control section saying unset means open', () => { + expect(readme).toContain('## Access control'); + const section = readme.slice(readme.indexOf('## Access control')); + expect(section.toLowerCase()).toContain('unset means open'); + }); + + test('.env.example lists the token so it is discoverable', () => { + expect(envExample).toMatch(/^FOUNDER_OS_ACCESS_TOKEN=/m); + }); +});