mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:58:51 +03:00
419 lines
19 KiB
Markdown
419 lines
19 KiB
Markdown
# Android Cast — Email agent config & 2FA / mobile auth flow
|
||
|
||
_Date: 2026-06-07 — FR §9 (email) + FR §10 (registration, 2FA, mobile) — **alpha must-have**_
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## Table of contents
|
||
|
||
<!-- toc -->
|
||
- [Document history](#document-history)
|
||
- [Executive summary](#executive-summary)
|
||
- [Current state (repo)](#current-state-repo)
|
||
- [§9 — Email addresses & agent config](#9-email-addresses-agent-config)
|
||
- [§9.1 Prerequisites](#91-prerequisites)
|
||
- [§9.2 Register addresses (free — DNS, not “buy mailboxes”)](#92-register-addresses-free-dns-not-buy-mailboxes)
|
||
- [§9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)](#93-forward-duplicate-to-gmail-info-foxxspambox0gmailcom)
|
||
- [§9.4 Recommended prod layout for this project](#94-recommended-prod-layout-for-this-project)
|
||
- [§9.5 Outbound mail from BE (verification emails)](#95-outbound-mail-from-be-verification-emails)
|
||
- [§10 — Registration, 2FA, mobile auth](#10-registration-2fa-mobile-auth)
|
||
- [§10.1 Registration & login stages](#101-registration-login-stages)
|
||
- [§10.2 Open-source building blocks (recommended)](#102-open-source-building-blocks-recommended)
|
||
- [§10.3 Users DB migration (privacy-first)](#103-users-db-migration-privacy-first)
|
||
- [§10.4 On-demand user maintenance (admin + self-service)](#104-on-demand-user-maintenance-admin-self-service)
|
||
- [§10.5 Operator checklist (track progress)](#105-operator-checklist-track-progress)
|
||
- [§10.6 Mobile-friendly UX (not BE-only)](#106-mobile-friendly-ux-not-be-only)
|
||
- [§10.7 Alpha implementation plan](#107-alpha-implementation-plan)
|
||
- [UI/UX alignment (existing consoles)](#uiux-alignment-existing-consoles)
|
||
- [Security & compliance notes](#security-compliance-notes)
|
||
- [Open questions](#open-questions)
|
||
- [Source linkage](#source-linkage)
|
||
<!-- /toc -->
|
||
|
||
**Documentation index:** [README.md](README.md)
|
||
|
||
---
|
||
|
||
## Document history
|
||
|
||
| Rev | Date | Focus |
|
||
|-----|------|--------|
|
||
| **1** | 2026-06-07 | Initial FR capture: email forwarding + registration/2FA for alpha; infra + BE + mobile flows. |
|
||
|
||
**Alpha gate:** agreed with project owners — user registration with verified email + second factor is **must-have for full alpha** (alongside RSSH remote access). LAN-only cast demo may still skip account flows.
|
||
|
||
---
|
||
|
||
## Executive summary
|
||
|
||
| Track | Goal |
|
||
|-------|------|
|
||
| **§9 Email** | Public-facing addresses `info@`, `admin@`, `root@` on **`apps.f0xx.org`** (and optional aliases on **`f0xx.org`**) that **forward** to personal Gmail; no paid mailbox SKU required. |
|
||
| **§10 Auth** | Replace “Register (coming soon)” with **email verification + flexible 2FA** (TOTP QR, optional WebAuthn/passkey on Android), captcha on abuse-prone endpoints, **privacy-preserving** audit tables, admin tools for root/slug admins. |
|
||
| **Mobile** | Same flows usable on phone browsers **and** deep links / Custom Tabs from the Android app where appropriate. |
|
||
| **Licenses** | Prefer **MIT / BSD / Apache-2.0** PHP libraries; avoid proprietary-only auth SaaS for core path. |
|
||
|
||
---
|
||
|
||
## Current state (repo)
|
||
|
||
| Area | Today |
|
||
|------|--------|
|
||
| BE login | Username + `password_hash` only — [`Auth.php`](../examples/crash_reporter/backend/src/Auth.php) |
|
||
| Registration | UI placeholder: “Register (coming soon)” — [`login.php`](../examples/crash_reporter/backend/views/login.php) |
|
||
| Users table | `username`, `password_hash`, `role` — no email, no 2FA — [`schema.mariadb.sql`](../examples/crash_reporter/backend/sql/schema.mariadb.sql) |
|
||
| Outbound email | **Not configured** — no SMTP in [`config.example.php`](../examples/crash_reporter/backend/config/config.example.php) |
|
||
| Mobile app | No account linking to crashes console yet |
|
||
| Roadmap | “User registration + email verification” — [`examples/crash_reporter/ROADMAP.md`](../examples/crash_reporter/ROADMAP.md) |
|
||
|
||
---
|
||
|
||
## §9 — Email addresses & agent config
|
||
|
||
Target addresses (any one canonical pair is fine; document both for flexibility):
|
||
|
||
| Role | Preferred | Alias |
|
||
|------|-----------|--------|
|
||
| Public / info | `info@apps.f0xx.org` | `info@f0xx.org` |
|
||
| Admin / ops | `admin@apps.f0xx.org` | `admin@f0xx.org`, `root@apps.f0xx.org` |
|
||
|
||
**Agent config** (future): BE `config.php` → `mail.from`, `mail.reply_to`, `mail.envelope_from` pointing at these addresses once DNS and forwarding work.
|
||
|
||
### §9.1 Prerequisites
|
||
|
||
You do **not** need to buy Google Workspace or Microsoft 365 mailboxes. You **do** need:
|
||
|
||
| # | Prerequisite | Why |
|
||
|---|--------------|-----|
|
||
| 1 | **DNS control** for `f0xx.org` and `apps.f0xx.org` | MX, SPF, DKIM, DMARC live at the DNS host (registrar or Cloudflare). |
|
||
| 2 | **Decide canonical mail domain** | Recommend **`apps.f0xx.org`** for product mail; keep `f0xx.org` as forward/alias only if desired. |
|
||
| 3 | **Inbound path** | MX records → free forwarder **or** self-hosted Postfix alias on FE/BE. |
|
||
| 4 | **Outbound path (BE)** | SMTP relay for verification/reset mail (see §9.5) — can be Gmail SMTP, SendGrid free tier, or local Postfix with SPF alignment. |
|
||
| 5 | **TLS on web** | Already have `https://apps.f0xx.org` — verification links use same vhost. |
|
||
| 6 | **Gmail destination** | e.g. `foxxspambox0@gmail.com` — receives forwarded + BCC copies. |
|
||
|
||
**Deliverability minimum (avoid spam folder):**
|
||
|
||
```text
|
||
SPF TXT @apps.f0xx.org "v=spf1 include:<forwarder-or-smtp-provider> ~all"
|
||
DKIM TXT (provider gives name/value)
|
||
DMARC TXT _dmarc.apps.f0xx.org "v=DMARC1; p=none; rua=mailto:admin@apps.f0xx.org"
|
||
```
|
||
|
||
Start with `p=none`; tighten after monitoring.
|
||
|
||
### §9.2 Register addresses (free — DNS, not “buy mailboxes”)
|
||
|
||
**“Register” here = DNS + forwarder rules**, not purchasing seats.
|
||
|
||
#### Option A — **Cloudflare Email Routing** (recommended if DNS is on Cloudflare)
|
||
|
||
1. Cloudflare dashboard → **Email** → **Email Routing** → enable for `apps.f0xx.org`.
|
||
2. Add **destination address** `foxxspambox0@gmail.com` (verify Gmail link once).
|
||
3. Create **Custom addresses**:
|
||
- `info@apps.f0xx.org` → forward to Gmail
|
||
- `admin@apps.f0xx.org` → forward to Gmail
|
||
- `root@apps.f0xx.org` → forward to Gmail (or same rule as admin)
|
||
4. Cloudflare adds MX automatically; copy **SPF** hint from dashboard.
|
||
5. Repeat for **`f0xx.org`** zone if you want `info@f0xx.org` (separate zone rules or redirect-only).
|
||
|
||
**Cost:** $0 on Cloudflare free plan.
|
||
|
||
#### Option B — **ImprovMX** (DNS at any registrar)
|
||
|
||
1. Sign up at [improvmx.com](https://improvmx.com) (free tier: limited aliases).
|
||
2. Add domain `apps.f0xx.org` → set MX to ImprovMX hosts (they show exact records).
|
||
3. Create aliases `info`, `admin`, `root` → `foxxspambox0@gmail.com`.
|
||
4. Add SPF include for ImprovMX.
|
||
|
||
#### Option C — **Self-hosted alias** (Gentoo FE or Alpine BE)
|
||
|
||
If you already run Postfix on FE:
|
||
|
||
```text
|
||
/etc/postfix/virtual:
|
||
info@apps.f0xx.org foxxspambox0@gmail.com
|
||
admin@apps.f0xx.org foxxspambox0@gmail.com
|
||
root@apps.f0xx.org foxxspambox0@gmail.com
|
||
```
|
||
|
||
Then `postmap /etc/postfix/virtual && systemctl reload postfix`. Requires **MX** pointing to FE public IP and port **25** reachable (often blocked on residential; OK on HVM with firewall rule).
|
||
|
||
**Do first:** pick **A or B** unless you explicitly want to operate SMTP on FE.
|
||
|
||
### §9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)
|
||
|
||
| Method | Inbox copy | Send-as From | IMAP on apps domain |
|
||
|--------|------------|--------------|---------------------|
|
||
| **Forward only** (Cloudflare / ImprovMX) | Yes — same subject/body in Gmail | Reply-from Gmail unless configured | N/A — read in Gmail |
|
||
| **Gmail “Send mail as”** | N/A | Can send as `info@apps.f0xx.org` if SMTP creds exist | Optional |
|
||
| **BCC archive** | Forward + hidden copy to second Gmail | — | Rarely needed |
|
||
|
||
For FR §9.3 (“same topic and body redirected”): **plain forward** is enough. Gmail shows original `To:` header; you can filter with Gmail rule `to:info@apps.f0xx.org`.
|
||
|
||
**Optional Gmail filter:**
|
||
|
||
```text
|
||
To: info@apps.f0xx.org → label: androidcast-info
|
||
```
|
||
|
||
No IMAP/POP3 server on `apps.f0xx.org` is required for this FR.
|
||
|
||
### §9.4 Recommended prod layout for this project
|
||
|
||
```text
|
||
Inbound (public):
|
||
info@apps.f0xx.org ──forward──► foxxspambox0@gmail.com
|
||
admin@apps.f0xx.org ──forward──► foxxspambox0@gmail.com
|
||
root@apps.f0xx.org ──forward──► foxxspambox0@gmail.com (same inbox or +tag)
|
||
|
||
Outbound (BE PHP):
|
||
From: Android Cast Issues <noreply@apps.f0xx.org>
|
||
Reply-To: info@apps.f0xx.org
|
||
SMTP: transactional relay (see §9.5)
|
||
```
|
||
|
||
Document chosen provider in [`INFRA.md`](INFRA.md) § changelog once live.
|
||
|
||
### §9.5 Outbound mail from BE (verification emails)
|
||
|
||
Add to `config.example.php` (implementation phase):
|
||
|
||
```php
|
||
'mail' => [
|
||
'transport' => 'smtp', // smtp | sendmail
|
||
'from' => 'Android Cast Issues <noreply@apps.f0xx.org>',
|
||
'reply_to' => 'info@apps.f0xx.org',
|
||
'smtp' => [
|
||
'host' => 'smtp.example.com',
|
||
'port' => 587,
|
||
'encryption' => 'tls',
|
||
'username' => '',
|
||
'password' => '', // env / secrets file, not git
|
||
],
|
||
],
|
||
```
|
||
|
||
**Free/low-cost SMTP options:**
|
||
|
||
| Provider | Notes |
|
||
|----------|--------|
|
||
| Gmail SMTP + App Password | OK for low volume; From may need “Send as” alignment |
|
||
| Brevo / SendGrid free tier | Better for `noreply@apps.f0xx.org` alignment |
|
||
| Local Postfix on BE | Full control; must publish SPF for BE egress IP |
|
||
|
||
PHP library: **Symfony Mailer** (MIT) — already common in PHP ecosystems.
|
||
|
||
---
|
||
|
||
## §10 — Registration, 2FA, mobile auth
|
||
|
||
### §10.1 Registration & login stages
|
||
|
||
Proposed **alpha** flow (flexible — toggles in config):
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as User (mobile browser)
|
||
participant BE as apps.f0xx.org PHP
|
||
participant M as Mail / Gmail
|
||
participant A as Android app (optional)
|
||
|
||
U->>BE: POST /register (email, username, password)
|
||
BE->>BE: captcha verify, rate limit
|
||
BE->>BE: store pending registration (hashed secrets)
|
||
BE->>M: Send verify link (24h token)
|
||
M->>U: Email with https://apps.f0xx.org/.../verify?token=
|
||
U->>BE: GET verify (or Custom Tab from app)
|
||
BE->>U: Prompt enroll 2FA (TOTP QR and/or WebAuthn)
|
||
U->>BE: POST /2fa/enroll (TOTP secret or passkey)
|
||
BE->>U: Account active → login
|
||
|
||
Note over U,BE: Login thereafter
|
||
U->>BE: username + password
|
||
BE->>U: 2FA challenge (TOTP code or WebAuthn)
|
||
U->>BE: verify second factor
|
||
BE->>U: Session cookie (existing ac_crash_sess)
|
||
```
|
||
|
||
| Stage | Method | Mobile-friendly |
|
||
|-------|--------|-----------------|
|
||
| **10.1.2 Email confirmation** | HTTPS link with signed token | Email app → Chrome Custom Tab; large tap target |
|
||
| **10.1.3 TOTP / QR** | Standard `otpauth://` QR + manual key | Scan with Google Authenticator / Aegis; or type 6 digits |
|
||
| **10.1.4 WebAuthn / passkey** | Platform authenticator (fingerprint) | **Android Chrome** + future app WebView if needed |
|
||
| **Captcha** | ALTCHA (MIT, self-hosted) or rate limit only at alpha | Invisible/PoW — no Google reCAPTCHA dependency |
|
||
| **Recovery** | Backup codes + optional second recovery email | Printable one-time codes; store hashed |
|
||
|
||
**Roles:** new self-registered users default to `viewer` + default company membership (existing RBAC). Elevation to `admin`/`root` remains **admin-only**.
|
||
|
||
### §10.2 Open-source building blocks (recommended)
|
||
|
||
| Function | Library | License | Notes |
|
||
|----------|---------|---------|-------|
|
||
| SMTP | [symfony/mailer](https://github.com/symfony/mailer) | MIT | Transport abstraction |
|
||
| TOTP | [spomky-labs/otphp](https://github.com/spomky-labs/otphp) | MIT | RFC 6238; QR via endroid/qr-code |
|
||
| WebAuthn | [web-auth/webauthn-lib](https://github.com/web-auth/webauthn-lib) | MIT | Passkeys; PHP 8.1+ |
|
||
| Password hash | `password_hash()` | PHP core | Already used |
|
||
| Captcha | [altcha-org/altcha](https://github.com/altcha-org/altcha) | MIT | Self-hosted PoW widget |
|
||
| JWT / tokens | Signed random + HMAC in DB | — | Prefer opaque tokens stored hashed |
|
||
|
||
**Avoid as hard dependency for alpha:** proprietary Auth0/Okta-only flows (OK as optional future SSO).
|
||
|
||
**Android app (phase 2 of alpha auth):**
|
||
|
||
| Feature | Approach |
|
||
|---------|----------|
|
||
| Open verify link | `CustomTabsIntent` → `https://apps.f0xx.org/.../verify` |
|
||
| TOTP | Optional in-app entry (Web login sufficient for alpha) |
|
||
| Passkey | Chrome on device; link account in settings later |
|
||
| Deep link | `androidcast://auth/verify?token=` optional — not required if HTTPS works |
|
||
|
||
### §10.3 Users DB migration (privacy-first)
|
||
|
||
New migration **`008_auth_email_2fa.sql`** (sketch — apply on MariaDB prod once implemented):
|
||
|
||
**Extend `users`:**
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| `email_normalized` | VARCHAR(254) NULL UNIQUE | Lowercase email |
|
||
| `email_verified_at` | TIMESTAMP NULL | NULL until link clicked |
|
||
| `status` | ENUM | `pending`, `active`, `locked`, `disabled` |
|
||
| `recovery_email_normalized` | VARCHAR(254) NULL | Optional second address |
|
||
|
||
**New tables (anonymous / hashed where possible):**
|
||
|
||
```sql
|
||
-- auth_pending_registrations: delete after verify or TTL
|
||
-- auth_factors: user_id, type ENUM('totp','webauthn','backup_code'), secret_or_credential_encrypted, label, created_at
|
||
-- auth_tokens: purpose ENUM('verify_email','reset_password','login_magic'), token_hash, expires_at, used_at
|
||
-- auth_attempts: ip_hash, username_hash, outcome, created_at (no raw IP in prod if avoidable)
|
||
-- auth_audit: actor_user_id, action, meta_json, created_at
|
||
```
|
||
|
||
**Privacy rules:**
|
||
|
||
- Store **hashed** opaque tokens (SHA-256), never raw token in DB.
|
||
- Store TOTP secrets **encrypted at rest** (libsodium `secretbox`, key in `config.php` env).
|
||
- `auth_attempts.ip_hash` = HMAC(server_pepper, ip) — supports rate limit without retaining IP.
|
||
- WebAuthn credentials: store `credential_id` + `public_key` only (standard).
|
||
|
||
SQLite dev: mirror schema in `Database::ensureSchema` like existing RBAC migrations.
|
||
|
||
### §10.4 On-demand user maintenance (admin + self-service)
|
||
|
||
| Action | Who | UI location |
|
||
|--------|-----|-------------|
|
||
| Add / change **recovery email** | User | `?view=account_security` (new) |
|
||
| Re-enroll TOTP / add passkey | User | Same; requires recent password + 2FA |
|
||
| Revoke passkeys / backup codes | User | Same |
|
||
| Clear failed login/register attempts | `root`, company `admin` | `?view=rbac` → user row → “Clear auth lockouts” |
|
||
| Disable user / force password reset | `root`, `platform_admin` | RBAC admin |
|
||
| View auth audit tail | `root` | `?view=auth_audit` (read-only) |
|
||
|
||
Session cookie path stays **`/app/androidcast_project`** (shared across crashes/build consoles).
|
||
|
||
### §10.5 Operator checklist (track progress)
|
||
|
||
Use this table during implementation (copy to ticket **#9 / #10**):
|
||
|
||
| # | Task | Owner | Done |
|
||
|---|------|-------|------|
|
||
| E1 | Enable Cloudflare Email Routing (or ImprovMX) for `apps.f0xx.org` | Ops | ☐ |
|
||
| E2 | Forward `info@`, `admin@`, `root@` → Gmail | Ops | ☐ |
|
||
| E3 | SPF + DKIM + DMARC records live | Ops | ☐ |
|
||
| E4 | Test inbound: send mail to `info@apps.f0xx.org`, arrives Gmail | Ops | ☐ |
|
||
| E5 | Configure BE SMTP + send test from staging | Dev | ☐ |
|
||
| A1 | Migration `008_auth_email_2fa.sql` + `Database::ensureSchema` | Dev | ☐ |
|
||
| A2 | Register + verify email views (EN/RU, `app.css`) | Dev | ☐ |
|
||
| A3 | TOTP enroll + login challenge | Dev | ☐ |
|
||
| A4 | WebAuthn enroll (optional alpha) | Dev | ☐ |
|
||
| A5 | ALTCHA on register/login | Dev | ☐ |
|
||
| A6 | Admin: clear lockouts + audit view | Dev | ☐ |
|
||
| A7 | Mobile soak: register on phone Chrome, verify, login | QA | ☐ |
|
||
| A8 | Document secrets in INFRA + config.example.php | Dev | ☐ |
|
||
|
||
### §10.6 Mobile-friendly UX (not BE-only)
|
||
|
||
| Requirement | Implementation |
|
||
|-------------|----------------|
|
||
| Viewport | Already `width=device-width` in [`layout.php`](../examples/crash_reporter/backend/views/layout.php) |
|
||
| Touch targets | Buttons ≥ 44px; reuse `.login-card`, `.btn` from [`app.css`](../examples/crash_reporter/backend/public/assets/css/app.css) |
|
||
| i18n | Extend [`en.json` / `ru.json`](../examples/crash_reporter/backend/public/assets/i18n/) |
|
||
| Email links | Single primary button “Confirm email” — no raw URL only |
|
||
| 2FA QR | Responsive `<img>` or SVG; “Can’t scan?” reveals manual secret |
|
||
| Errors | Same `.alert` pattern as login |
|
||
| Android app | Settings → “Project account” → opens Custom Tab to account/security (post-alpha polish OK) |
|
||
|
||
### §10.7 Alpha implementation plan
|
||
|
||
| Phase | Scope | Estimate |
|
||
|-------|--------|----------|
|
||
| **P0 — Email infra** | §9 DNS + forward + outbound SMTP test | 0.5–1 day ops |
|
||
| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 2–3 days |
|
||
| **P2 — TOTP login** | Enroll QR + challenge on login | 1–2 days |
|
||
| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day |
|
||
| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 1–2 days optional for alpha |
|
||
| **P5 — Mobile app link** | Custom Tab entry points | 0.5 day |
|
||
|
||
**Alpha sign-off (auth):** new user can register on phone, verify email, enroll TOTP, log in with 2FA; admin can clear lockout; inbound `info@` mail reaches Gmail.
|
||
|
||
Update [`ALPHA.md`](ALPHA.md) § G (auth) and [`ROADMAP.md`](ROADMAP.md) alpha backend checklist when P1 starts.
|
||
|
||
---
|
||
|
||
## UI/UX alignment (existing consoles)
|
||
|
||
Do **not** introduce a new design system. Reuse:
|
||
|
||
| Asset | Path |
|
||
|-------|------|
|
||
| Styles | `public/assets/css/app.css` — `.login-page`, `.login-card`, `.alert`, `.muted`, `.locale-picker` |
|
||
| Layout shell | `views/layout.php` — nav, hub card styling |
|
||
| i18n | `public/assets/js/i18n.js` + JSON catalogs |
|
||
| Analytics hook | `AnalyticsHead::render()` on auth pages (optional) |
|
||
|
||
New views (planned):
|
||
|
||
- `views/register.php`
|
||
- `views/verify_email.php`
|
||
- `views/account_security.php`
|
||
- `views/two_factor_challenge.php`
|
||
|
||
Hub landing contact form (`landing-pages.inc.php`) should eventually use **`info@apps.f0xx.org`** as `mailto:` — align when email live.
|
||
|
||
---
|
||
|
||
## Security & compliance notes
|
||
|
||
- Enforce **rate limits** on register, login, verify resend (e.g. 5/min/IP hash).
|
||
- Lock account after N failed 2FA attempts; unlock via email or admin.
|
||
- Do **not** log passwords or TOTP codes.
|
||
- Backup codes: generate 10 one-time codes, display once, store bcrypt hashes.
|
||
- GDPR-style minimization: optional `auth_attempts` retention job (purge > 90 days).
|
||
|
||
---
|
||
|
||
## Open questions
|
||
|
||
| # | Question | Default proposal |
|
||
|---|----------|------------------|
|
||
| Q1 | Require WebAuthn for alpha or TOTP-only? | **TOTP required**; WebAuthn optional P4 |
|
||
| Q2 | Allow registration on prod immediately or invite-only? | **Open register** with captcha; `viewer` role |
|
||
| Q3 | Single Gmail inbox for all aliases? | Yes for now (`foxxspambox0@gmail.com`) |
|
||
| Q4 | Separate `noreply@` for outbound? | Yes — no forward needed; SPF only |
|
||
|
||
---
|
||
|
||
## Source linkage
|
||
|
||
| Topic | Doc / code |
|
||
|-------|------------|
|
||
| Infra / DNS | [INFRA.md](INFRA.md) |
|
||
| Crash console | [CRASH_REPORTER.md](CRASH_REPORTER.md), [backend README](../examples/crash_reporter/backend/README.md) |
|
||
| RBAC | [backend README § RBAC](../examples/crash_reporter/backend/README.md#rbac-phase-1--admin-ui) |
|
||
| Alpha gates | [ALPHA.md](ALPHA.md), [ROADMAP.md](ROADMAP.md) |
|
||
| PDF build | [`scripts/build-all-docs-pdf.sh`](../scripts/build-all-docs-pdf.sh) |
|