docs: update the plan, specs and deploy guide for the setup toggle (D-045)
- PROJECT_PLAN.md: rewrite §5.1 for the toggle gate, mark D-025 superseded, refresh D-026 / O-8 / A13 / §12 and add the D-045 change-log entry. - SPEC-005: §1/§3/§5/§6 (AC-1/AC-2/AC-7 + AC-8) reflect the three states and the optional code; §9 marks the D-044 tunnel bits superseded; new §10. - SPEC-003: §4/§7 pipeline + AC-7/AC-10/AC-14; new §13 (D-045); D-025 ref removed from the governing decisions. - docs/deploy-zoraxy.md: §5 rewritten around the toggle + optional code; §1/§2/§3/§7 de-tunnelled; see-also updated. - README: first-run + deployment sections rewritten (enable setup: true, open /setup, complete, the app flips it off). - config.example.yaml: document the toggle and the optional code.
This commit is contained in:
parent
c7229e23ff
commit
9fdc9eded6
6 changed files with 268 additions and 166 deletions
|
|
@ -98,7 +98,7 @@ DOOR (top level)
|
||||||
/{domain}/ → 302 → /home (the ONE canonical redirect)
|
/{domain}/ → 302 → /home (the ONE canonical redirect)
|
||||||
/{domain}/home → neutral landing page (public, no user)
|
/{domain}/home → neutral landing page (public, no user)
|
||||||
/{domain}/access → password / access recovery
|
/{domain}/access → password / access recovery
|
||||||
/{domain}/setup → the bootstrap wizard (setup mode · loopback only)
|
/{domain}/setup → the bootstrap wizard (the `setup` toggle, D-045)
|
||||||
/{domain}/api | static | media | ws | dev → reserved infrastructure
|
/{domain}/api | static | media | ws | dev → reserved infrastructure
|
||||||
/{domain}/holy-shit → 404 (unknown · not reserved · not a GUID)
|
/{domain}/holy-shit → 404 (unknown · not reserved · not a GUID)
|
||||||
|
|
||||||
|
|
@ -287,7 +287,7 @@ the highest role, the **root of the chain**. When the `users` table is empty, th
|
||||||
### 5.1 First run — the `/setup` wizard
|
### 5.1 First run — the `/setup` wizard
|
||||||
|
|
||||||
```
|
```
|
||||||
fresh deploy → config.setup: true → setup mode: `/` and `/setup` show a first-run page
|
fresh deploy → config.setup: true → `/setup` serves the WIZARD
|
||||||
Step 1 the domain / base URL
|
Step 1 the domain / base URL
|
||||||
Step 2 the top admin — name, email, **TOTP** enrollment (the passkey is added at first
|
Step 2 the top admin — name, email, **TOTP** enrollment (the passkey is added at first
|
||||||
sign-in, D-041) (user_id = 1, role: admin)
|
sign-in, D-041) (user_id = 1, role: admin)
|
||||||
|
|
@ -297,18 +297,20 @@ fresh deploy → config.setup: true → setup mode: `/` and `/setup` show a firs
|
||||||
```
|
```
|
||||||
|
|
||||||
- `setup` is a **reserved `PARAM1`** (always — so no user can ever be named `setup`).
|
- `setup` is a **reserved `PARAM1`** (always — so no user can ever be named `setup`).
|
||||||
- **The bootstrap is gated — never first-visitor-wins (D-025 · D-044).** In setup mode the **wizard**
|
- **The gate is the config toggle `setup` (D-045).** Three states:
|
||||||
is served to **either** **(a)** a **direct tunnel** — a loopback TCP peer with **no**
|
- **`setup: true`** → `/setup` serves the **wizard** (the operator's deliberate switch); `GET /`
|
||||||
`X-Forwarded-For` — **or** **(b)** a request **unlocked with the printed SETUP CODE** (D-044).
|
points at it; every other route is 404.
|
||||||
Before it is unlocked, `GET /` and `GET /setup` show a friendly **first-run page** (never a bare
|
- **`setup: false` and no admin** → the app is **LOCKED**: `GET /` and `GET /setup` show a
|
||||||
404, so the domain does not look "down"); every other route is 404.
|
**boilerplate** telling the operator to set `setup: true` and restart; every other route is 404.
|
||||||
- **The setup code (D-044).** A **128-bit** code is generated at first run and printed in a
|
**This state is not publicly claimable.**
|
||||||
**startup banner** (`setup_code` in config overrides it); it is **single-use** and
|
- **completion** → the app writes **`setup: false`** and creates the admin → productive.
|
||||||
**rate-limited**. This is the general gate for the **LXC/Proxmox** topology, where an SSH tunnel
|
- **The setup code is OPTIONAL (D-044 · D-045).** When `setup_code` is set it is a **second factor**
|
||||||
is impractical (the tunnel would land on the container *gateway*, not loopback).
|
(the code form comes first; single-use · rate-limited · redacted — A8). Empty ⇒ the toggle alone
|
||||||
- **It supersedes `{bootstrap_guid}`** — the wizard *is* the bootstrap; the operator unlocks it (the
|
admits. *(D-044's generated-code banner, the peer/tunnel gate D-025, and the bare 404 are retired.)*
|
||||||
code, or an SSH tunnel to `127.0.0.1:8080`) and completes it, then exposes the instance.
|
- **It supersedes `{bootstrap_guid}`** — the wizard *is* the bootstrap; the operator enables setup and
|
||||||
- *README: do NOT expose MyDay publicly before the first admin exists.*
|
completes it, then exposes the instance.
|
||||||
|
- *README: do NOT expose MyDay publicly before the first admin exists; set `setup_code` (or complete
|
||||||
|
setup immediately) because while `setup: true` first-visitor-wins is possible (D-045 residual).*
|
||||||
|
|
||||||
### 5.2 Admin recovery — a config-gated enrolment link
|
### 5.2 Admin recovery — a config-gated enrolment link
|
||||||
|
|
||||||
|
|
@ -466,8 +468,8 @@ the top admin (first user)
|
||||||
| **D-022** | 2026-10-13 | **Bootstrap:** `users` empty → **setup mode**; the top admin is created at a **gated** path; setup ends at the first user. | **Gate it** (loopback and/or setup token) or "first user" = "first visitor". |
|
| **D-022** | 2026-10-13 | **Bootstrap:** `users` empty → **setup mode**; the top admin is created at a **gated** path; setup ends at the first user. | **Gate it** (loopback and/or setup token) or "first user" = "first visitor". |
|
||||||
| **D-023** | 2026-10-13 | **Admin recovery = `reset_admin` config toggle** — rotate the top admin's identity to `pending_enrollment`, log the new GUID; audited; auto-clears. | The config is the offline-held root of trust. |
|
| **D-023** | 2026-10-13 | **Admin recovery = `reset_admin` config toggle** — rotate the top admin's identity to `pending_enrollment`, log the new GUID; audited; auto-clears. | The config is the offline-held root of trust. |
|
||||||
| **D-024** | 2026-10-13 | **Roles (KISS): `admin > director > manager > team`.** You may create only roles **below** your own; the new user's manager defaults to the creator. | Answers "who may create a user?" |
|
| **D-024** | 2026-10-13 | **Roles (KISS): `admin > director > manager > team`.** You may create only roles **below** your own; the new user's manager defaults to the creator. | Answers "who may create a user?" |
|
||||||
| **D-025** *(rev. 2026-10-15b)* | 2026-10-13 | **The bootstrap is gated — never first-visitor-wins.** In **setup mode**, `/setup` is served to a request that is **either** **(a)** a **direct tunnel** — **TCP peer loopback *and* no `X-Forwarded-For`** — **or** **(b)** **unlocked with the setup code** (D-044). Anything else → the **boilerplate**/404. The app may bind `0.0.0.0` (D-043). | **Re-revised**: a nested **LXC** tunnel lands on the container **gateway**, not loopback — so gate (a) alone is unusable there. The **setup code** (D-044) is the general gate. |
|
| ~~D-025~~ *(rev. 2026-10-15b)* | 2026-10-13 | ~~**The bootstrap is gated — never first-visitor-wins.** In **setup mode**, `/setup` is served to a request that is **either** **(a)** a **direct tunnel** — **TCP peer loopback *and* no `X-Forwarded-For`** — **or** **(b)** **unlocked with the setup code** (D-044).~~ | **SUPERSEDED by D-045** — the peer/tunnel gate is retired; the gate is the `setup` toggle (the **setup code** stays as an optional second factor). |
|
||||||
| **D-026** | 2026-10-13 | **A `/setup` web wizard is the bootstrap** (a reserved `PARAM1`). It configures the **domain**, creates the **top admin** (user 1) with Passkey + TOTP, and mail/locale — **setup-mode only · the setup gate (D-025/D-044)** — then the app runs productive. `config.setup` forces the mode. | Supersedes `{bootstrap_guid}`; configuration through a UI instead of hand-editing config. |
|
| **D-026** | 2026-10-13 | **A `/setup` web wizard is the bootstrap** (a reserved `PARAM1`). It configures the **domain**, creates the **top admin** (user 1) with Passkey + TOTP, and mail/locale — **only while the `setup` toggle is on (D-045)** — then the app runs productive. `config.setup` forces the mode. | Supersedes `{bootstrap_guid}`; configuration through a UI instead of hand-editing config. |
|
||||||
| **D-027** | 2026-10-13 | **Modular architecture.** The **core is `MyDay.auth`** (identity · session · roles · the door · the **module registry**); features are **modules** mounted at **`/{GUID}/{module}/{function}/{param}`**. A module depends only on core interfaces and **owns its own tables**. | The natural generalisation of the `PARAM2` switch — the same URL shape now carries a plugin model. Makes `expense`/IDM additive, not invasive. |
|
| **D-027** | 2026-10-13 | **Modular architecture.** The **core is `MyDay.auth`** (identity · session · roles · the door · the **module registry**); features are **modules** mounted at **`/{GUID}/{module}/{function}/{param}`**. A module depends only on core interfaces and **owns its own tables**. | The natural generalisation of the `PARAM2` switch — the same URL shape now carries a plugin model. Makes `expense`/IDM additive, not invasive. |
|
||||||
| **D-028** | 2026-10-13 | **Drop-in modules, discovered at BUILD time.** A module is a directory (`internal/modules/<name>/`) that **self-registers** via `init()`; a **generated aggregator** blank-imports every module dir, so the directory is the source of truth. **No runtime plugins.** | The **Caddy** pattern. Drop a dir → `go generate` → rebuild. Deterministic order; log the loaded set at startup. |
|
| **D-028** | 2026-10-13 | **Drop-in modules, discovered at BUILD time.** A module is a directory (`internal/modules/<name>/`) that **self-registers** via `init()`; a **generated aggregator** blank-imports every module dir, so the directory is the source of truth. **No runtime plugins.** | The **Caddy** pattern. Drop a dir → `go generate` → rebuild. Deterministic order; log the loaded set at startup. |
|
||||||
| **D-029** | 2026-10-13 | **User management is CORE, not a module.** Identities, roles, and the chain live in `MyDay.auth`. | "Management" decomposes into **core (users)** + `requests` (workflow) + `hr` (records). |
|
| **D-029** | 2026-10-13 | **User management is CORE, not a module.** Identities, roles, and the chain live in `MyDay.auth`. | "Management" decomposes into **core (users)** + `requests` (workflow) + `hr` (records). |
|
||||||
|
|
@ -485,7 +487,8 @@ the top admin (first user)
|
||||||
| **D-041** | 2026-10-14 | **The setup wizard enrolls TOTP only.** Any **TOTP login with no passkey** is **nudged** to enroll one, with *not now* / ***don't ask again*** (`users.passkey_opt_out`). The nudge is **suppressed while the origin is loopback** (or in setup mode). | Resolves **O-13**: a passkey enrolled over loopback binds RP ID `127.0.0.1` and is unusable at the public domain. Passkeys are encouraged, not forced — TOTP stays a valid factor (D-005). |
|
| **D-041** | 2026-10-14 | **The setup wizard enrolls TOTP only.** Any **TOTP login with no passkey** is **nudged** to enroll one, with *not now* / ***don't ask again*** (`users.passkey_opt_out`). The nudge is **suppressed while the origin is loopback** (or in setup mode). | Resolves **O-13**: a passkey enrolled over loopback binds RP ID `127.0.0.1` and is unusable at the public domain. Passkeys are encouraged, not forced — TOTP stays a valid factor (D-005). |
|
||||||
| **D-042** | 2026-10-14 | **TOTP-only enrollment is specific to the bootstrap admin (`/setup`).** **Invited users enroll both factors** (Passkey + TOTP) at the real origin; the passkey **nudge** remains the safety net for any identity without a passkey. | Resolves **O-14** (owner, Option A): keep the **strongest default** where the origin permits it. |
|
| **D-042** | 2026-10-14 | **TOTP-only enrollment is specific to the bootstrap admin (`/setup`).** **Invited users enroll both factors** (Passkey + TOTP) at the real origin; the passkey **nudge** remains the safety net for any identity without a passkey. | Resolves **O-14** (owner, Option A): keep the **strongest default** where the origin permits it. |
|
||||||
| **D-043** | 2026-10-15 | **Deployment: behind Zoraxy.** The app listens on **`0.0.0.0:8080` over HTTP**; **TLS terminates at Zoraxy** at **`10.1.0.10`**; the public origin is **`https://myday.sechpoint.app`** (`base_url`). `trusted_proxies` = `["10.1.0.10"]` (Zoraxy). The app **honours `X-Forwarded-Proto`** from the proxy to set `Secure` / `__Host-` cookies and HSTS. | The app is **not** the TLS endpoint, so it must honour the proxy's scheme header. **Firewall `:8080` to Zoraxy only.** |
|
| **D-043** | 2026-10-15 | **Deployment: behind Zoraxy.** The app listens on **`0.0.0.0:8080` over HTTP**; **TLS terminates at Zoraxy** at **`10.1.0.10`**; the public origin is **`https://myday.sechpoint.app`** (`base_url`). `trusted_proxies` = `["10.1.0.10"]` (Zoraxy). The app **honours `X-Forwarded-Proto`** from the proxy to set `Secure` / `__Host-` cookies and HSTS. | The app is **not** the TLS endpoint, so it must honour the proxy's scheme header. **Firewall `:8080` to Zoraxy only.** |
|
||||||
| **D-044** | 2026-10-15 | **First-run access = a printed SETUP CODE + a boilerplate page.** On first run the app generates a **128-bit setup code** and prints it in a **startup banner** (overridable via `setup.code` in config). `GET /setup` shows a **first-run boilerplate** with a **code-entry form** — *never a bare 404*, so the domain does not look "down"; the **correct code unlocks the wizard**. The code is **single-use**, **rate-limited**, and **redacted from the logs** (A8). | The topology (**LXC/Proxmox + Zoraxy**) makes an SSH tunnel impractical — so the gate is a **code**, not a peer. Grafana/Jupyter-style: it still closes first-visitor-wins. |
|
| **D-044** | 2026-10-15 | **First-run access = a printed SETUP CODE + a boilerplate page.** On first run the app generates a **128-bit setup code** and prints it in a **startup banner** (overridable via `setup_code` in config). `GET /setup` shows a **first-run boilerplate** with a **code-entry form** — *never a bare 404*, so the domain does not look "down"; the **correct code unlocks the wizard**. The code is **single-use**, **rate-limited**, and **redacted from the logs** (A8). | The topology (**LXC/Proxmox + Zoraxy**) makes an SSH tunnel impractical — so the gate is a **code**, not a peer. Grafana/Jupyter-style: it still closes first-visitor-wins. **Amended by D-045 — the code is now OPTIONAL.** |
|
||||||
|
| **D-045** | 2026-10-15 | **The setup gate is the config toggle `setup`.** `setup: true` ⇒ `/setup` serves the wizard **at the public domain**; completing it writes **`setup: false`**. `setup: false` **and no admin** ⇒ a **boilerplate** telling the operator to enable setup (that state is **not** publicly claimable). The **setup code** (`setup_code`) becomes **OPTIONAL** — a second factor **when set**. The **peer/tunnel gate is retired** (D-025 superseded). | Owner's model — an LXC+Zoraxy deployment can't tunnel, and the toggle is the operator's deliberate switch. **Residual:** while `setup: true`, whoever reaches `/setup` first can claim the admin — so **set `setup_code`** or complete setup immediately. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -499,7 +502,7 @@ the top admin (first user)
|
||||||
| O-5 | `reset_admin` semantics | **RESOLVED** (D-033): rotate the identity · **config approval flag** · email the enrolment link to user 1's address on record · flag cleared. |
|
| O-5 | `reset_admin` semantics | **RESOLVED** (D-033): rotate the identity · **config approval flag** · email the enrolment link to user 1's address on record · flag cleared. |
|
||||||
| O-6 | GUID placement for actions | **RESOLVED** (D-032): the GUID **rides the path**; `do` / `action` carry the module/function, never identity. |
|
| O-6 | GUID placement for actions | **RESOLVED** (D-032): the GUID **rides the path**; `do` / `action` carry the module/function, never identity. |
|
||||||
| O-7 | One top admin or several | **RESOLVED** — **exactly one** ("the Highlander"). The root of the chain; recovered via D-033. |
|
| O-7 | One top admin or several | **RESOLVED** — **exactly one** ("the Highlander"). The root of the chain; recovered via D-033. |
|
||||||
| O-8 | Bootstrap gate | **RESOLVED** (D-025, re-revised 2026-10-15b): the **tunnel OR the setup code** (D-044), enforced **per request**; the boilerplate replaces the bare 404. |
|
| O-8 | Bootstrap gate | **RESOLVED** (D-045): the gate is the **`setup` config toggle**; the **setup code** (D-044) is an **optional** second factor. The peer/tunnel rule (D-025) is **superseded**; the boilerplate replaces the bare 404. **Residual:** while `setup: true`, first-visitor-wins unless `setup_code` is set. |
|
||||||
| O-9 | "KISS" = the **principle** | **RESOLVED** (D-034): a **simple tiered model, no ACL** — `admin > director > manager > team`; a role **implies all lower roles**; roles assigned via **multi-select tick boxes**. |
|
| O-9 | "KISS" = the **principle** | **RESOLVED** (D-034): a **simple tiered model, no ACL** — `admin > director > manager > team`; a role **implies all lower roles**; roles assigned via **multi-select tick boxes**. |
|
||||||
| O-10 | Where settings live | **RESOLVED** (D-035): **the config file** — settings are simpler to change there than via a DB migration. The **DB holds operational data only**. |
|
| O-10 | Where settings live | **RESOLVED** (D-035): **the config file** — settings are simpler to change there than via a DB migration. The **DB holds operational data only**. |
|
||||||
| O-11 | The `requests` engine (data-model) | **RESOLVED / build-time** — the dispatch is settled (D-032 · D-039). Whether `requests` is a generic engine or `absence`/`time` own their flows is an **internal data-model choice inside the module**, decided at build (M3). **Not a plan-blocker.** |
|
| O-11 | The `requests` engine (data-model) | **RESOLVED / build-time** — the dispatch is settled (D-032 · D-039). Whether `requests` is a generic engine or `absence`/`time` own their flows is an **internal data-model choice inside the module**, decided at build (M3). **Not a plan-blocker.** |
|
||||||
|
|
@ -532,7 +535,7 @@ the top admin (first user)
|
||||||
| A10 | GUID **tombstoned, never recycled** | HIGH | designed (D-008) |
|
| A10 | GUID **tombstoned, never recycled** | HIGH | designed (D-008) |
|
||||||
| A11 | Every auth event audited (bootstrap, enroll, login, recovery request/approve/deny, revoke, `reset_admin`) | HIGH | open (L4c: `Sessions.GC` · `UpdateWebAuthnSignCount` unaudited) |
|
| A11 | Every auth event audited (bootstrap, enroll, login, recovery request/approve/deny, revoke, `reset_admin`) | HIGH | open (L4c: `Sessions.GC` · `UpdateWebAuthnSignCount` unaudited) |
|
||||||
| A12 | Changing the **email on record** is a privileged, audited operation | HIGH | open |
|
| A12 | Changing the **email on record** is a privileged, audited operation | HIGH | open |
|
||||||
| A13 | **Setup window hardened** — `/setup` serves the **wizard** only in setup mode and only to **either** the **direct tunnel** (a loopback TCP peer **with no `X-Forwarded-For`**) **or** a request **unlocked with the printed setup code** (D-025 re-revised · D-044); `GET /` and `/setup` show the **first-run boilerplate** (never a bare 404), every other route 404; the `setup` module **pinned at the door (M1c/L3s)**; the unlock **rate-limited** with the `/setup` class (A3) and the code **redacted** (A8); `/setup` → 404 once productive | HIGH | **enforced** |
|
| A13 | **Setup window hardened** — `/setup` serves the **wizard** only while the **`setup` toggle** is on (D-045); `setup: false` **and no admin** ⇒ a **locked boilerplate** at `/` and `/setup` (never a bare 404), every other route 404; when **`setup_code` is set** the code form comes first (D-044) — the code **single-use** and **rate-limited** (A3), the unlock **redacted** (A8); the `setup` module **pinned at the door (M1c/L3s)**; `/setup` → 404 once productive. **Residual:** while `setup: true`, first-visitor-wins unless `setup_code` is set. | HIGH | **enforced** |
|
||||||
| A14 | **Top admin protected** — cannot be deleted; `reset_admin` rotations audited; startup warning while setup mode is reachable | HIGH | open |
|
| A14 | **Top admin protected** — cannot be deleted; `reset_admin` rotations audited; startup warning while setup mode is reachable | HIGH | open |
|
||||||
| A15 | **Pre-session CSRF** — the anonymous forms (`/setup` · `/access` · enroll/login) need a signed **double-submit** cookie (SPEC-005/006/007) | HIGH | **enforced** — signing keys generated at startup in setup mode (M1s) |
|
| A15 | **Pre-session CSRF** — the anonymous forms (`/setup` · `/access` · enroll/login) need a signed **double-submit** cookie (SPEC-005/006/007) | HIGH | **enforced** — signing keys generated at startup in setup mode (M1s) |
|
||||||
|
|
||||||
|
|
@ -559,7 +562,7 @@ the top admin (first user)
|
||||||
|---|---|
|
|---|---|
|
||||||
| **git repo + `.gitignore`** | `git init`; the real `config.yaml` is excluded **from the very first commit** (D-010); `config.example.yaml` is committed. |
|
| **git repo + `.gitignore`** | `git init`; the real `config.yaml` is excluded **from the very first commit** (D-010); `config.example.yaml` is committed. |
|
||||||
| **`AGENT.md`** | the repo's **operating instructions for AI agents**: what MyDay is, the doctrine (§3–§4), the **rule of the door**, the stack, and a pointer to **this plan as the source of truth**. |
|
| **`AGENT.md`** | the repo's **operating instructions for AI agents**: what MyDay is, the doctrine (§3–§4), the **rule of the door**, the stack, and a pointer to **this plan as the source of truth**. |
|
||||||
| **`README.md`** | human quickstart — build, run, config, **and the first-run rule: do NOT expose MyDay publicly before the first admin exists; unlock the wizard with the printed setup code (or an SSH tunnel to `127.0.0.1:8080`).** |
|
| **`README.md`** | human quickstart — build, run, config, **and the first-run rule: do NOT expose MyDay publicly before the first admin exists; enable `setup: true` and (optionally) set `setup_code` to claim the wizard.** |
|
||||||
| **`docs/`** | this plan at the root; specs later in `docs/specs/`. |
|
| **`docs/`** | this plan at the root; specs later in `docs/specs/`. |
|
||||||
| **CI (later)** | build + test on push. |
|
| **CI (later)** | build + test on push. |
|
||||||
|
|
||||||
|
|
@ -656,3 +659,5 @@ shell/layout lives in `core/web`; a module's views live with the module.
|
||||||
| 2026-10-15 | **D-025 (revised) + D-043 ✅ IMPLEMENTED.** **Bind refusal removed** — `server.CheckSetupBind` / `ErrNonLoopbackSetup` / `server.IsLoopback` deleted; `Serve` binds any address (the deployment binds `0.0.0.0:8080`). **Setup gate is per request and strict** — `httpx.SetupRequestAllowed` requires a **loopback TCP peer AND no `X-Forwarded-For`**, evaluated on the RAW request (captured before `ClientIP` SETS the header); anything else → **404 before dispatch**; an unparseable peer fails closed (AC-7/AC-10). **Scheme is per request (D-043)** — `httpx.RequestIsHTTPS` (direct TLS, or a **trusted** proxy's `X-Forwarded-Proto`), carried on the request context (`httpx.WithSecure`), so the session cookie, the **`__Host-`** prefix, the pre-session/ceremony cookies and **HSTS** all agree; the plain-http tunnel keeps the **bare, non-Secure** cookie — which also fixes the latent `config.example.yaml` bootstrap (an `https://` `base_url` no longer force-marks the cookie Secure over the tunnel). `config.IsSecure` **removed** (dead). `config.example.yaml` documents the Zoraxy topology + the **firewall-`:8080`-to-Zoraxy-only** rule; the README gains a **Deployment** section and the tunnel-only bootstrap. Syncs **SPEC-003 §4/§6/§7/§9** and **SPEC-005 §3/§5/§6**. Gate green (`build` · `vet` · `test -race` — 18 pkgs) · **static CGO-free** binary · `routing` purity intact. Live smoke (real binary): `/setup` via the tunnel → **200**, `+ X-Forwarded-For` → **404**, `/home` → **404**, `X-Forwarded-Proto: https` → **HSTS + Secure** cookie, plain http → **neither**. |
|
| 2026-10-15 | **D-025 (revised) + D-043 ✅ IMPLEMENTED.** **Bind refusal removed** — `server.CheckSetupBind` / `ErrNonLoopbackSetup` / `server.IsLoopback` deleted; `Serve` binds any address (the deployment binds `0.0.0.0:8080`). **Setup gate is per request and strict** — `httpx.SetupRequestAllowed` requires a **loopback TCP peer AND no `X-Forwarded-For`**, evaluated on the RAW request (captured before `ClientIP` SETS the header); anything else → **404 before dispatch**; an unparseable peer fails closed (AC-7/AC-10). **Scheme is per request (D-043)** — `httpx.RequestIsHTTPS` (direct TLS, or a **trusted** proxy's `X-Forwarded-Proto`), carried on the request context (`httpx.WithSecure`), so the session cookie, the **`__Host-`** prefix, the pre-session/ceremony cookies and **HSTS** all agree; the plain-http tunnel keeps the **bare, non-Secure** cookie — which also fixes the latent `config.example.yaml` bootstrap (an `https://` `base_url` no longer force-marks the cookie Secure over the tunnel). `config.IsSecure` **removed** (dead). `config.example.yaml` documents the Zoraxy topology + the **firewall-`:8080`-to-Zoraxy-only** rule; the README gains a **Deployment** section and the tunnel-only bootstrap. Syncs **SPEC-003 §4/§6/§7/§9** and **SPEC-005 §3/§5/§6**. Gate green (`build` · `vet` · `test -race` — 18 pkgs) · **static CGO-free** binary · `routing` purity intact. Live smoke (real binary): `/setup` via the tunnel → **200**, `+ X-Forwarded-For` → **404**, `/home` → **404**, `X-Forwarded-Proto: https` → **HSTS + Secure** cookie, plain http → **neither**. |
|
||||||
| 2026-10-15 | **Deployment docs for D-043 (Zoraxy).** Added **`docs/deploy-zoraxy.md`** (topology · the app `config.yaml` · the Zoraxy **ProxyHost** with `X-Forwarded-For` + `X-Forwarded-Proto` · the **firewall `:8080` from `10.1.0.10` only** · the **tunnel-only bootstrap** + the restart that adopts the persisted secrets · the **systemd** install · the **misconfig warnings**) and **`deploy/myday.service`** (a `Type=simple` · `Restart=on-failure` unit with `User=`/`WorkingDirectory=`/`ExecStart=`/`After=network-online.target`). `config.example.yaml` now sets **`trusted_proxies: ["10.1.0.10"]`** (Zoraxy) with the silent-failure + firewall caveats; the README Deployment section points at the guide. **Docs/deploy only — no code change**; gate green. |
|
| 2026-10-15 | **Deployment docs for D-043 (Zoraxy).** Added **`docs/deploy-zoraxy.md`** (topology · the app `config.yaml` · the Zoraxy **ProxyHost** with `X-Forwarded-For` + `X-Forwarded-Proto` · the **firewall `:8080` from `10.1.0.10` only** · the **tunnel-only bootstrap** + the restart that adopts the persisted secrets · the **systemd** install · the **misconfig warnings**) and **`deploy/myday.service`** (a `Type=simple` · `Restart=on-failure` unit with `User=`/`WorkingDirectory=`/`ExecStart=`/`After=network-online.target`). `config.example.yaml` now sets **`trusted_proxies: ["10.1.0.10"]`** (Zoraxy) with the silent-failure + firewall caveats; the README Deployment section points at the guide. **Docs/deploy only — no code change**; gate green. |
|
||||||
| 2026-10-15 | **D-044 ✅ IMPLEMENTED — the setup code + the first-run boilerplate (D-025 re-revised).** The real topology is **LXC (11020) behind Proxmox + Zoraxy**, so the SSH tunnel is impractical (it lands on the container *gateway*, not loopback). `core/setup` gains a **`Gate`** — a **128-bit setup code** (generated at first run, printed in an unmistakable **startup banner**; `setup_code` overrides it) plus a **signed unlock cookie** (`myday_setup`, carrying a keyed one-way **fingerprint**, never the code). The door (`Deps.SetupGate`) now serves the **wizard** to **either** the tunnel **(a)** or an **unlocked** request **(b)**; `GET /` and an un-unlocked `GET /setup` render a friendly **first-run page** with the code form — **never a bare 404**, so the domain stops looking "down" — and every other route stays 404. The unlock is **single-use**, **rate-limited** (the `/setup` A3 class), **CSRF-protected (A15)** and **redacted** (A8); a wrong/absent code gets a **generic 403**. New: `httpx.Signer.Fingerprint` · `config.setup_code` · `setup.Gate` (`gate.go`) · the `unlock` action · `server.SetupGate`/`serveBoilerplate`. Docs: `deploy-zoraxy.md` §5 rewritten for the LXC/Proxmox + code flow (tunnel kept as gate a), README first-run/deploy sections, SPEC-003 §7/§9 (AC-7 · AC-10 · AC-14) and SPEC-005 §3/§4/§6/§9. Gate green (`build` · `vet` · `test -race`) · **static CGO-free** binary · `routing` purity intact. **Live smoke** (real binary, proxy-style `X-Forwarded-For`): the banner prints the code · `/` and `/setup` → **200 boilerplate** · `/home` → **404** · the tunnel → the wizard · wrong code → **403 generic** · correct code → **303** + unlock cookie → the wizard · a spent code → **403** · the code absent from the access log and redacted when hand-typed into the query. |
|
| 2026-10-15 | **D-044 ✅ IMPLEMENTED — the setup code + the first-run boilerplate (D-025 re-revised).** The real topology is **LXC (11020) behind Proxmox + Zoraxy**, so the SSH tunnel is impractical (it lands on the container *gateway*, not loopback). `core/setup` gains a **`Gate`** — a **128-bit setup code** (generated at first run, printed in an unmistakable **startup banner**; `setup_code` overrides it) plus a **signed unlock cookie** (`myday_setup`, carrying a keyed one-way **fingerprint**, never the code). The door (`Deps.SetupGate`) now serves the **wizard** to **either** the tunnel **(a)** or an **unlocked** request **(b)**; `GET /` and an un-unlocked `GET /setup` render a friendly **first-run page** with the code form — **never a bare 404**, so the domain stops looking "down" — and every other route stays 404. The unlock is **single-use**, **rate-limited** (the `/setup` A3 class), **CSRF-protected (A15)** and **redacted** (A8); a wrong/absent code gets a **generic 403**. New: `httpx.Signer.Fingerprint` · `config.setup_code` · `setup.Gate` (`gate.go`) · the `unlock` action · `server.SetupGate`/`serveBoilerplate`. Docs: `deploy-zoraxy.md` §5 rewritten for the LXC/Proxmox + code flow (tunnel kept as gate a), README first-run/deploy sections, SPEC-003 §7/§9 (AC-7 · AC-10 · AC-14) and SPEC-005 §3/§4/§6/§9. Gate green (`build` · `vet` · `test -race`) · **static CGO-free** binary · `routing` purity intact. **Live smoke** (real binary, proxy-style `X-Forwarded-For`): the banner prints the code · `/` and `/setup` → **200 boilerplate** · `/home` → **404** · the tunnel → the wizard · wrong code → **403 generic** · correct code → **303** + unlock cookie → the wizard · a spent code → **403** · the code absent from the access log and redacted when hand-typed into the query. |
|
||||||
|
| 2026-10-15 | **Setup gate simplified to the config toggle (D-045).** `setup: true` enables `/setup` at the **public domain**; **completion writes `setup: false`**; `setup: false` + no admin → a **locked boilerplate** (not claimable); the **setup code becomes OPTIONAL**; the **peer/tunnel gate is retired** (D-025 superseded). **Residual:** while `setup: true`, whoever reaches `/setup` first can claim the admin — set `setup_code` or finish immediately. SPEC-005 §3/§4 revised. |
|
||||||
|
| 2026-10-15 | **D-045 ✅ IMPLEMENTED — the toggle gate, the optional code, the retired tunnel.** The gate is `config.setup`: **`setup: true`** → `/setup` serves the **wizard** (or the **code form** first when `setup_code` is set, D-044); **`setup: false` + no admin** → the **LOCKED** state (`/` and `/setup` show an "enable setup" boilerplate, every other route 404, **not claimable**); **completion** writes **`setup: false`**. `setup.New` **no longer generates a code** — blank ⇒ `Gate.Required()` false ⇒ the toggle alone admits. **Retired:** `httpx.SetupRequestAllowed` + `httpx.IsLoopbackIP` (deleted) and the `!bootstrapped()` fail-safe (now the locked predicate, latched at startup). **Kept:** the XFF/XFP handling for the **client IP** and the `Secure`/`__Host-` cookies + HSTS (A6/A11/D-043). New door seams: `Deps.LockedMode`, `SetupGate.{Required,CodeForm,Landing,Locked}`; `Gate` renders three first-run pages. The **startup banner** states the mode and prints the code **only when required**; the locked state prints *"set `setup: true` and restart"*. Docs: SPEC-003 §7/§9 (AC-7 · AC-10 · AC-14), SPEC-005 §1/§3/§5/§6/§9/§10, `deploy-zoraxy.md` §1/§2/§3/§5/§7, README first-run/deploy, `config.example.yaml`, A13 · O-8 · §5.1 · D-025 superseded. Gate green (`go build` · `go vet` · `go test -race` — 18 pkgs) · **static CGO-free** binary · `routing` purity intact. **Live smoke** (real binary, all three states): locked → `/`·`/setup` **200 boilerplate**, `/home` **404**, unlock **404**; `setup: true` no code → `/setup` **wizard**, `/` **landing**, `/home` **404**; `setup: true` + code → **code form** → wrong code **403** → correct code **303** + unlock cookie → **wizard**; full bootstrap (`domain.save`→`admin.create`→`admin.totp.begin/verify`→`finish`) → `config.yaml` **`setup: false`** → `/setup` **404**, `/` **302**. |
|
||||||
|
|
|
||||||
78
README.md
78
README.md
|
|
@ -38,75 +38,83 @@ go build -o myday ./cmd/myday
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ First run — the setup code
|
## ⚠️ First run — enable setup, then complete the wizard
|
||||||
|
|
||||||
The **first user becomes the top admin** (the root of the chain). MyDay bootstraps through a
|
The **first user becomes the top admin** (the root of the chain). Setup is gated by the **`setup`
|
||||||
**`/setup` wizard** that is **gated — never first-visitor-wins** (D-025/D-044). On first start it
|
config toggle** (D-045): enable it, start MyDay, complete the wizard — the app then writes
|
||||||
prints a **one-time 128-bit SETUP CODE** in its startup log:
|
**`setup: false`** and the instance becomes productive.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
======================================================================
|
# config.yaml
|
||||||
MyDay is NOT set up yet — no admin exists.
|
setup: true # enable the /setup wizard
|
||||||
======================================================================
|
setup_code: "" # OPTIONAL second factor — set it if the domain is reachable by others
|
||||||
Open the setup wizard at:
|
|
||||||
https://myday.sechpoint.app/setup
|
|
||||||
SETUP CODE: 9f3a-1b2c-…
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open the domain (or `http://localhost:8080`) and enter that code at `/setup`. **Until then every
|
1. **Start MyDay.** The startup banner states the mode and prints the setup URL.
|
||||||
page is a friendly first-run page — never a bare 404** — and every other route answers 404. Read
|
2. **Open the wizard at `/setup`** (e.g. `https://myday.sechpoint.app/setup`).
|
||||||
the code again with:
|
- `setup_code` **empty** → the wizard is served directly;
|
||||||
|
- `setup_code` **set** → the code form comes first; enter the code printed in the startup banner.
|
||||||
|
3. **Complete the wizard** — domain → top admin (TOTP) → mail/locale. It creates the admin and
|
||||||
|
writes **`setup: false`** to `config.yaml`.
|
||||||
|
4. **Restart** so the app adopts the persisted secrets: `systemctl restart myday`.
|
||||||
|
|
||||||
|
While `setup: true` and setup is unfinished:
|
||||||
|
|
||||||
|
- `GET /` points at the wizard, and `GET /setup` serves it (or the code form);
|
||||||
|
- **every other route answers 404** — there is no app yet.
|
||||||
|
|
||||||
|
While `setup: false` and **no admin exists**, the instance is **locked**: `GET /` and `GET /setup`
|
||||||
|
show a page telling you to set `setup: true` and restart, and every other route 404. This state is
|
||||||
|
**not publicly claimable** — a fresh instance never exposes its bootstrap by accident.
|
||||||
|
|
||||||
|
> **Residual risk:** while `setup: true`, whoever reaches `/setup` first can claim the admin.
|
||||||
|
> **Set `setup_code`** (or complete setup immediately) if the domain is open to others. The code is
|
||||||
|
> **single-use** and the entry form is **rate-limited**.
|
||||||
|
|
||||||
|
Read the code again (when one is set):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
journalctl -u myday # inside the container
|
journalctl -u myday # inside the container
|
||||||
pct exec 11020 -- journalctl -u myday # from the Proxmox host
|
pct exec 11020 -- journalctl -u myday # from the Proxmox host
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Why a code, not a tunnel?** MyDay's reference deployment is an **LXC container behind
|
|
||||||
> Proxmox + Zoraxy**. An SSH tunnel is impractical there: `pct enter`/SSH lands on the container
|
|
||||||
> **gateway**, not loopback, so a tunnel to `127.0.0.1:8080` cannot be formed. The setup code
|
|
||||||
> closes first-visitor-wins instead. A **direct SSH tunnel** to the app's loopback port still works
|
|
||||||
> as a second gate and needs **no code** (D-025 gate a).
|
|
||||||
|
|
||||||
> **Note:** the code is **single-use** and the entry form is **rate-limited**. Complete the wizard,
|
|
||||||
> then **restart** so the app adopts the `config.yaml` the wizard wrote.
|
|
||||||
|
|
||||||
> **Bonus:** to use the tunnel gate, reach the app over **`localhost` / `127.0.0.1`** — only a
|
|
||||||
> loopback origin counts as a secure context for the browser's **WebAuthn** dialogs. Over plain http
|
|
||||||
> the cookies are sent without `Secure` (required by the tunnel bootstrap); behind the proxy (https)
|
|
||||||
> they are hardened — see *Deployment*.
|
|
||||||
|
|
||||||
Only **after** the top admin exists should you put MyDay behind a reverse proxy and expose it.
|
Only **after** the top admin exists should you put MyDay behind a reverse proxy and expose it.
|
||||||
|
|
||||||
|
> **Note:** the setup wizard enrolls **TOTP**; the admin adds a **passkey** at first sign-in on the
|
||||||
|
> real (https) origin, via the passkey nudge.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment — behind Zoraxy (TLS at the proxy)
|
## Deployment — behind Zoraxy (TLS at the proxy)
|
||||||
|
|
||||||
> **Full walkthrough:** [`docs/deploy-zoraxy.md`](docs/deploy-zoraxy.md) — topology · config ·
|
> **Full walkthrough:** [`docs/deploy-zoraxy.md`](docs/deploy-zoraxy.md) — topology · config ·
|
||||||
> the Zoraxy ProxyHost · the firewall rule · the first-run setup code (no tunnel needed) · systemd
|
> the Zoraxy ProxyHost · the firewall rule · the **`setup: true` bootstrap** · systemd ·
|
||||||
> · the misconfig warnings. This section is the summary.
|
> the misconfig warnings. This section is the summary.
|
||||||
|
|
||||||
The reference deployment terminates **TLS at [Zoraxy](https://github.com/tobychui/zoraxy)** at
|
The reference deployment terminates **TLS at [Zoraxy](https://github.com/tobychui/zoraxy)** at
|
||||||
**`10.1.0.10`**; the app itself speaks **plain HTTP**. Config (`config.example.yaml` →
|
**`10.1.0.10`**; the app itself speaks **plain HTTP**. Config (`config.example.yaml` →
|
||||||
`config.yaml`, D-043):
|
`config.yaml`, D-043):
|
||||||
|
|
||||||
- **App** — `server.listen: "0.0.0.0:8080"`; the app binds any address (there is **no** setup-mode
|
- **App** — `server.listen: "0.0.0.0:8080"`; the app binds any address (there is **no** setup-mode
|
||||||
bind refusal, D-025 revised).
|
bind refusal).
|
||||||
- **Proxy** — Zoraxy terminates TLS and forwards to `http://<app-host>:8080`, setting
|
- **Proxy** — Zoraxy terminates TLS and forwards to `http://<app-host>:8080`, setting
|
||||||
`X-Forwarded-For` and `X-Forwarded-Proto`.
|
`X-Forwarded-For` and `X-Forwarded-Proto`.
|
||||||
- **`trusted_proxies`** — `["10.1.0.10"]` (Zoraxy). The app **honours `X-Forwarded-Proto`** from a
|
- **`trusted_proxies`** — `["10.1.0.10"]` (Zoraxy). The app **honours `X-Forwarded-Proto`** from a
|
||||||
trusted proxy to decide a request is `https`, and then sets the session cookie **`Secure` +
|
trusted proxy to decide a request is `https`, and then sets the session cookie **`Secure` +
|
||||||
`__Host-`** and sends **HSTS**. Over plain http (the tunnel) it keeps the bare, non-`Secure` cookie
|
`__Host-`** and sends **HSTS**. Over plain http it keeps the bare, non-`Secure` cookie.
|
||||||
so the bootstrap works. A **wrong value fails silently** — no `Secure` cookie, no HSTS.
|
A **wrong value fails silently** — no `Secure` cookie, no HSTS.
|
||||||
- **`base_url`** — `https://myday.sechpoint.app`: the public origin, used in invite/recovery links.
|
- **`base_url`** — `https://myday.sechpoint.app`: the public origin, used in invite/recovery links.
|
||||||
- **Firewall** — **`:8080` MUST be reachable only from `10.1.0.10`.** Anything else that can reach
|
- **Firewall** — **`:8080` MUST be reachable only from `10.1.0.10`.** Anything else that can reach
|
||||||
the port bypasses the proxy **and its TLS**; the app would then serve plain HTTP to that client.
|
the port bypasses the proxy **and its TLS**; the app would then serve plain HTTP to that client.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# read the one-time setup code, then open the domain and enter it at /setup
|
# on a fresh instance: enable setup, start MyDay, then open the domain at /setup
|
||||||
|
# config.yaml: setup: true
|
||||||
|
# (optional) setup_code: "…" # a second factor; the printed code comes first
|
||||||
|
sudo systemctl restart myday
|
||||||
|
# read the mode / setup code in the log:
|
||||||
journalctl -u myday # inside the container
|
journalctl -u myday # inside the container
|
||||||
pct exec 11020 -- journalctl -u myday # from the Proxmox host
|
pct exec 11020 -- journalctl -u myday # from the Proxmox host
|
||||||
# (or, for the no-code tunnel gate: ssh -L 8080:127.0.0.1:8080 user@server)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Deploying MyDay behind Zoraxy
|
# Deploying MyDay behind Zoraxy
|
||||||
|
|
||||||
> **Status:** the reference deployment for **D-043** / **D-044**. The app is never the TLS
|
> **Status:** the reference deployment for **D-043** / **D-044** / **D-045**. The app is never the
|
||||||
> endpoint; **[Zoraxy](https://github.com/tobychui/zoraxy) terminates TLS** and
|
> TLS endpoint; **[Zoraxy](https://github.com/tobychui/zoraxy) terminates TLS** and
|
||||||
> relays plain HTTP to the app. This document is operator-facing — it changes no
|
> relays plain HTTP to the app. This document is operator-facing — it changes no
|
||||||
> code. The decision of record is `PROJECT_PLAN.md` **D-043** / **D-025 (re-revised 2026-10-15b)**;
|
> code. The decision of record is `PROJECT_PLAN.md` **D-043** / **D-045** (the setup gate;
|
||||||
> the gate's behaviour is `docs/specs/spec-003-core-door.md` **§7** and
|
> **D-025 retired**); the gate's behaviour is `docs/specs/spec-003-core-door.md` **§7** and
|
||||||
> `docs/specs/spec-005-setup-wizard.md` **§3/§4**.
|
> `docs/specs/spec-005-setup-wizard.md` **§3/§4**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -25,9 +25,9 @@ browser ──https──▶ Zoraxy 10.1.0.10 ──http──▶ app 0.0.0.0
|
||||||
app is not the TLS endpoint it must **honour the proxy's `X-Forwarded-Proto`** to
|
app is not the TLS endpoint it must **honour the proxy's `X-Forwarded-Proto`** to
|
||||||
know a request arrived over `https` (that is what turns on the `Secure` +
|
know a request arrived over `https` (that is what turns on the `Secure` +
|
||||||
`__Host-` session cookie and HSTS).
|
`__Host-` session cookie and HSTS).
|
||||||
- **Container** — MyDay runs in an **LXC container (id `11020`)** under Proxmox. This is why
|
- **Container** — MyDay runs in an **LXC container (id `11020`)** under Proxmox. That is why the
|
||||||
the first-run gate is a **setup code** and not just an SSH tunnel: SSH/`pct enter` lands on the
|
first-run gate is the **`setup` config toggle** rather than an SSH tunnel: SSH/`pct enter` lands
|
||||||
container **gateway**, not loopback, so a tunnel to `127.0.0.1:8080` cannot be formed (§5).
|
on the container **gateway**, not loopback, so a tunnel to `127.0.0.1:8080` cannot be formed (§5).
|
||||||
- **Firewall** — `:8080` is reachable **only from `10.1.0.10`** (see §4). The port
|
- **Firewall** — `:8080` is reachable **only from `10.1.0.10`** (see §4). The port
|
||||||
is the app's whole attack surface: anything that can reach it bypasses both
|
is the app's whole attack surface: anything that can reach it bypasses both
|
||||||
Zoraxy **and its TLS**.
|
Zoraxy **and its TLS**.
|
||||||
|
|
@ -47,8 +47,8 @@ server:
|
||||||
- "10.1.0.10" # Zoraxy
|
- "10.1.0.10" # Zoraxy
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`listen`** — any bind is allowed (there is **no** setup-mode bind refusal,
|
- **`listen`** — any bind is allowed (there is **no** setup-mode bind refusal).
|
||||||
D-025 revised). Bind `0.0.0.0:8080`; the firewall narrows who can reach it.
|
Bind `0.0.0.0:8080`; the firewall narrows who can reach it.
|
||||||
- **`base_url`** — the public origin. It is the host the browser actually visits,
|
- **`base_url`** — the public origin. It is the host the browser actually visits,
|
||||||
so it must be `https://` (WebAuthn derives its Relying Party ID from it).
|
so it must be `https://` (WebAuthn derives its Relying Party ID from it).
|
||||||
- **`trusted_proxies`** — the peers whose `X-Forwarded-For` / `X-Forwarded-Proto`
|
- **`trusted_proxies`** — the peers whose `X-Forwarded-For` / `X-Forwarded-Proto`
|
||||||
|
|
@ -79,9 +79,8 @@ The app deliberately **requires** both:
|
||||||
|
|
||||||
- `X-Forwarded-Proto: https` is what makes the session cookie `Secure`
|
- `X-Forwarded-Proto: https` is what makes the session cookie `Secure`
|
||||||
(`__Host-myday_session`) and adds HSTS (AC-13).
|
(`__Host-myday_session`) and adds HSTS (AC-13).
|
||||||
- `X-Forwarded-For` is what keeps the setup gate's tunnel test (gate a, §5) honest:
|
- `X-Forwarded-For` is what makes the **audit log** and rate limiting see the real
|
||||||
a request with **no** `X-Forwarded-For` from a loopback peer is the direct tunnel,
|
client (A6) instead of the proxy.
|
||||||
not a proxy relay.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -109,36 +108,54 @@ table inet myday_guard {
|
||||||
|
|
||||||
> If you run a host firewall, this rule is the load-bearing one: without it the
|
> If you run a host firewall, this rule is the load-bearing one: without it the
|
||||||
> app's plain-HTTP port is directly reachable and its TLS hardening (cookie
|
> app's plain-HTTP port is directly reachable and its TLS hardening (cookie
|
||||||
> `Secure`, HSTS, the tunnel gate) is bypassed.
|
> `Secure`, HSTS) is bypassed. It also means the `setup: true` toggle is the only
|
||||||
|
> thing standing between a random client and the wizard — see §5.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. First-run bootstrap — the setup code (no tunnel needed)
|
## 5. First-run bootstrap — the `setup` toggle
|
||||||
|
|
||||||
The **first user becomes the top admin**, so the initial `/setup` wizard is gated **per request**
|
The **first user becomes the top admin**, so the initial `/setup` wizard is gated by the **`setup`
|
||||||
— **never first-visitor-wins** (D-025 · D-044). There are **two** ways in:
|
config toggle** (D-045) — **never exposed by accident**. There are three states:
|
||||||
|
|
||||||
| Gate | Who | Needs a code? |
|
| `setup` | admin exists? | Behaviour |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **(a) the direct SSH tunnel** — a loopback TCP peer **with no `X-Forwarded-For`** | a workstation that can tunnel to the app's loopback port | no |
|
| **`false`** | **no** | **Locked.** `GET /` and `GET /setup` show *"MyDay is not set up — set `setup: true` in `config.yaml` and restart."* Every other route → **404**. **Not claimable.** |
|
||||||
| **(b) the printed SETUP CODE** | anyone who can read the server log, over the public domain | yes |
|
| **`true`** | either | `/setup` serves the **wizard** (the operator's deliberate switch). `GET /` points at it. Every other route → **404**. |
|
||||||
|
| **`false`** | **yes** | **Productive** — the app serves normally. |
|
||||||
|
|
||||||
**The setup code is the general gate for this topology.** MyDay generates a **128-bit** code at
|
**To bootstrap:**
|
||||||
first start and prints it in an unmistakable **startup banner**:
|
|
||||||
|
```yaml
|
||||||
|
# config.yaml — on the container
|
||||||
|
setup: true
|
||||||
|
setup_code: "" # optional: a second factor (see below)
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart myday
|
||||||
|
# read the mode / the code in the log:
|
||||||
|
journalctl -u myday | grep -i 'setup'
|
||||||
|
# then open the wizard:
|
||||||
|
# https://myday.sechpoint.app/setup
|
||||||
|
```
|
||||||
|
|
||||||
|
The startup banner states the mode and prints the setup URL:
|
||||||
|
|
||||||
```
|
```
|
||||||
======================================================================
|
======================================================================
|
||||||
MyDay is NOT set up yet — no admin exists.
|
MyDay is in SETUP MODE — no admin exists yet.
|
||||||
======================================================================
|
======================================================================
|
||||||
Open the setup wizard at:
|
Open the setup wizard at:
|
||||||
https://myday.sechpoint.app/setup
|
https://myday.sechpoint.app/setup
|
||||||
SETUP CODE: 9f3a-1b2c-4d5e-6f70-8192-a3b4-c5d6-e7f8
|
======================================================================
|
||||||
Read the code again:
|
|
||||||
journalctl -u myday (in the container)
|
|
||||||
pct exec 11020 -- journalctl -u myday (from the Proxmox host)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Read the code** (in the LXC container, or from Proxmox):
|
**The optional setup code (D-044 · D-045).** Set `setup_code` and the wizard requires it — the code
|
||||||
|
form comes first (read the code from the startup log). Leave it empty and the toggle alone admits,
|
||||||
|
so the wizard is served directly.
|
||||||
|
|
||||||
|
**Read the code again** (when one is set):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# inside the container
|
# inside the container
|
||||||
|
|
@ -148,17 +165,18 @@ journalctl -u myday | grep -i 'setup code'
|
||||||
pct exec 11020 -- journalctl -u myday | grep -i 'setup code'
|
pct exec 11020 -- journalctl -u myday | grep -i 'setup code'
|
||||||
```
|
```
|
||||||
|
|
||||||
Then **open `https://myday.sechpoint.app/setup`** and enter the code. Until the code is entered:
|
Until setup is finished: `GET /` points at the wizard, `GET /setup` serves it (or the code form),
|
||||||
|
and **every other route answers 404**. The code is **single-use** and the entry form is
|
||||||
|
**rate-limited** (wrong code → a generic 403).
|
||||||
|
|
||||||
- `GET /` and `GET /setup` show a **friendly first-run page** (with the code form) — **never a bare
|
> **Residual risk — `setup: true` is not claimable-proof.** While the toggle is on, whoever
|
||||||
404**, so the domain does not look "down";
|
> reaches `/setup` first can claim the admin. **Set `setup_code`** (or complete setup immediately)
|
||||||
- **every other route** answers **404**;
|
> if the domain is reachable by anyone else. This is the owner's deliberate trade (D-045): the
|
||||||
- the code is **single-use** and the entry form is **rate-limited** (wrong code → a generic 403).
|
> toggle is the operator's switch; the code is an optional lock.
|
||||||
|
|
||||||
> **No tunnel required.** The old guidance ("bootstrap over an SSH tunnel first") no longer
|
> **No tunnel required.** The old guidance ("bootstrap over an SSH tunnel first") no longer applies:
|
||||||
> applies: in LXC a tunnel to `127.0.0.1:8080` cannot be formed (it lands on the container
|
> in LXC a tunnel to `127.0.0.1:8080` cannot be formed (it lands on the container gateway), and the
|
||||||
> gateway), which is exactly why D-044 replaced it with the code. The tunnel gate (a) still works
|
> peer/tunnel gate (**D-025**) is **retired** by D-045.
|
||||||
> and needs no code, if you have a route to the app's loopback port.
|
|
||||||
|
|
||||||
The wizard creates the top admin (**TOTP**; the passkey is added at first sign-in on the real
|
The wizard creates the top admin (**TOTP**; the passkey is added at first sign-in on the real
|
||||||
origin — D-041) and writes `config.yaml` (`setup: false`, the base URL, mail/locale, and the
|
origin — D-041) and writes `config.yaml` (`setup: false`, the base URL, mail/locale, and the
|
||||||
|
|
@ -176,12 +194,12 @@ sudo systemctl restart myday
|
||||||
|
|
||||||
The restart **adopts the written `config.yaml`** — the generated keys are now on
|
The restart **adopts the written `config.yaml`** — the generated keys are now on
|
||||||
disk, so the admin's TOTP secret (sealed with that key) keeps working, and the
|
disk, so the admin's TOTP secret (sealed with that key) keeps working, and the
|
||||||
**setup code is gone** (`GET /setup` → 404). **Boot the productive instance from the written
|
**wizard is off** (`setup: false`; `GET /setup` → 404). **Boot the productive instance from the
|
||||||
`config.yaml`, never from `config.example.yaml`** (its placeholders fail closed in productive mode
|
written `config.yaml`, never from `config.example.yaml`** (its placeholders fail closed in productive
|
||||||
by design).
|
mode by design).
|
||||||
|
|
||||||
> **Pinning your own code.** Set `setup_code:` in `config.yaml` to override the generated one
|
> **Pinning your own code.** Set `setup_code:` in `config.yaml` to require a code for the wizard
|
||||||
> (then protect the file). Leave it blank to have MyDay generate and print a fresh code each boot.
|
> (then protect the file). Leave it blank to have the `setup: true` toggle admit on its own.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -223,17 +241,16 @@ curl -sI https://myday.sechpoint.app/ | grep -i '^strict-transport-security'
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Session cookie has **no `Secure`**, **no `__Host-`** prefix, and **no HSTS** | `trusted_proxies` is **empty or wrong**, so `X-Forwarded-Proto` is ignored (fails *silently*) | Set `server.trusted_proxies: ["10.1.0.10"]` and restart |
|
| Session cookie has **no `Secure`**, **no `__Host-`** prefix, and **no HSTS** | `trusted_proxies` is **empty or wrong**, so `X-Forwarded-Proto` is ignored (fails *silently*) | Set `server.trusted_proxies: ["10.1.0.10"]` and restart |
|
||||||
| Client IP in the audit log is the **proxy**, not the user | same — `X-Forwarded-For` is not trusted | as above |
|
| Client IP in the audit log is the **proxy**, not the user | same — `X-Forwarded-For` is not trusted | as above |
|
||||||
| `GET /` and `GET /setup` show a **first-run page** with a code box | **by design** — MyDay is not set up yet (D-044) | read the setup code (§5) and enter it; complete the wizard |
|
| `GET /` points at the wizard and `GET /setup` serves it (or a code box) | **by design** — `setup: true`, the wizard is enabled (D-045) | complete the wizard; if a code is required, read it from the log (§5) |
|
||||||
| Any **other** route returns **404** while bootstrapping | **by design** — only `/` and `/setup` are served in setup mode (SPEC-005 §3) | complete the wizard; the app serves normally once productive |
|
| `GET /` and `GET /setup` show *"MyDay is not set up — set `setup: true`…"* | **by design** — `setup: false` and **no admin** (the LOCKED state) | set `setup: true` in `config.yaml` and restart (§5) |
|
||||||
| The setup code is **rejected** (403) | it is **single-use** (already spent) or mistyped | restart MyDay to print a fresh code, or pin `setup_code:` in `config.yaml` (§5) |
|
| Any **other** route returns **404** while bootstrapping | **by design** — only `/` and `/setup` are served while the wizard is enabled (SPEC-005 §3) | complete the wizard; the app serves normally once productive |
|
||||||
| `/setup` returns **404** after the wizard | **by design** — setup is complete | re-enter only via `setup: true` + restart |
|
| The setup code is **rejected** (403) | it is **single-use** (already spent), mistyped, or `setup_code` was changed | check the code in `config.yaml` / the log; restart to re-arm |
|
||||||
|
| `/setup` returns **404** after the wizard | **by design** — setup is complete (`setup: false`) | re-enter only via `setup: true` + restart |
|
||||||
|
|
||||||
> **The load-bearing deployment requirement:** Zoraxy **must set
|
> **The load-bearing deployment requirement:** Zoraxy **must set `X-Forwarded-Proto`** (or the
|
||||||
> `X-Forwarded-For`**. It is what distinguishes a proxy relay from the direct
|
> session cookie silently loses `Secure`/`__Host-` and HSTS) and **`X-Forwarded-For`** (or the audit
|
||||||
> tunnel (gate a): a proxied request with **no** `X-Forwarded-For` would look like
|
> log records the proxy, not the client). Neither is a setup gate any more (D-045) — the gate is the
|
||||||
> the tunnel and open the wizard without a code. (The **setup code** still closes
|
> `setup` toggle; set `setup_code` if you want a lock on top of it.
|
||||||
> first-visitor-wins for anything the proxy *does* mark.)
|
|
||||||
> `X-Forwarded-Proto` is what keeps the cookie `Secure` over the proxy.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -241,8 +258,8 @@ curl -sI https://myday.sechpoint.app/ | grep -i '^strict-transport-security'
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`PROJECT_PLAN.md`](../PROJECT_PLAN.md) | D-043 · D-025 (re-revised) · D-044 |
|
| [`PROJECT_PLAN.md`](../PROJECT_PLAN.md) | D-043 · D-045 (the setup gate) · D-044 (the optional code) |
|
||||||
| [`docs/specs/spec-003-core-door.md`](specs/spec-003-core-door.md) | §7 the setup gate · §9 AC-7 / AC-10 / AC-13 / AC-14 |
|
| [`docs/specs/spec-003-core-door.md`](specs/spec-003-core-door.md) | §7 the setup gate · §9 AC-7 / AC-10 / AC-13 / AC-14 |
|
||||||
| [`docs/specs/spec-005-setup-wizard.md`](specs/spec-005-setup-wizard.md) | §3/§4 the bootstrap + the setup code |
|
| [`docs/specs/spec-005-setup-wizard.md`](specs/spec-005-setup-wizard.md) | §3/§4 the bootstrap + the optional code |
|
||||||
| [`config.example.yaml`](../config.example.yaml) | the commented config template |
|
| [`config.example.yaml`](../config.example.yaml) | the commented config template |
|
||||||
| [`deploy/myday.service`](../deploy/myday.service) | the systemd unit |
|
| [`deploy/myday.service`](../deploy/myday.service) | the systemd unit |
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
| **Created** | 2026-10-13 |
|
| **Created** | 2026-10-13 |
|
||||||
| **Author** | OpenArchitect |
|
| **Author** | OpenArchitect |
|
||||||
| **Target milestone** | **M0** |
|
| **Target milestone** | **M0** |
|
||||||
| **Governing decisions** | **D-002** · **D-003** · **D-025** · **D-031** · **D-039** · `PROJECT_PLAN.md` §4/§12 |
|
| **Governing decisions** | **D-002** · **D-003** · **D-031** · **D-039** · **D-043** · **D-045** · `PROJECT_PLAN.md` §4/§12 |
|
||||||
| **Related** | SPEC-002 (routing) · SPEC-004 (identity) |
|
| **Related** | SPEC-002 (routing) · SPEC-004 (identity) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -48,10 +48,11 @@ request
|
||||||
3. security headers — X-Content-Type-Options · Referrer-Policy · a strict CSP · HSTS (when https)
|
3. security headers — X-Content-Type-Options · Referrer-Policy · a strict CSP · HSTS (when https)
|
||||||
· Permissions-Policy · COOP · CORP [L4s]
|
· Permissions-Policy · COOP · CORP [L4s]
|
||||||
4. routing.Dispatch() — the pure model (SPEC-002)
|
4. routing.Dispatch() — the pure model (SPEC-002)
|
||||||
5. setup-mode gate — in setup mode a request is served ONLY if it is the direct tunnel
|
5. setup gate — the gate is the config toggle `setup` (D-045). While `setup: true`
|
||||||
(TCP peer loopback AND no XFF) OR unlocked with the setup code (D-044);
|
`/setup` serves the wizard (the code form first when `setup_code` is
|
||||||
`GET /` and an un-unlocked `GET /setup` get the first-run boilerplate,
|
set, D-044); `GET /` points at it; everything else 404. `setup: false`
|
||||||
everything else 404 (§7)
|
with no admin is the LOCKED state: `/` and `/setup` get the "enable
|
||||||
|
setup" boilerplate, everything else 404 (§7)
|
||||||
6. switch on Kind:
|
6. switch on Kind:
|
||||||
Redirect → 302
|
Redirect → 302
|
||||||
NotFound → 404
|
NotFound → 404
|
||||||
|
|
@ -115,17 +116,18 @@ type RequestContext struct {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Setup gate (D-025 re-revised 2026-10-15 · D-044)
|
## 7. Setup gate (D-045)
|
||||||
|
|
||||||
- **No bind refusal.** The app **may bind any address** — `0.0.0.0:8080` behind **Zoraxy** (D-043).
|
- **No bind refusal.** The app **may bind any address** — `0.0.0.0:8080` behind **Zoraxy** (D-043).
|
||||||
- **While in setup mode**, `/setup` is served when **either**:
|
- **The gate is the config toggle `setup`** (D-045; the peer/tunnel rule of D-025 is **retired**).
|
||||||
- **(a)** the request is a **direct tunnel** — **TCP peer loopback** **and** **no `X-Forwarded-For`**
|
- **`setup: true`** → `/setup` serves the **wizard**. If `setup_code` is set the **code form**
|
||||||
(an empty/unparseable peer fails closed). *A same-host proxy presents a loopback peer, so the
|
comes first (D-044) and the unlock is a signed cookie; otherwise the toggle alone admits.
|
||||||
absence of XFF is the proof the request did not traverse one;* **or**
|
`GET /` points at the wizard; **every other route is 404**.
|
||||||
- **(b)** the request is **unlocked with the setup code** (D-044) — the boilerplate's code form.
|
- **`setup: false`, no admin** → the **LOCKED** state: `GET /` and `GET /setup` show an
|
||||||
- Anything else in setup mode → the **first-run boilerplate** (`/` · `/setup`) or **404**.
|
"enable setup" boilerplate; every other route is 404. Not publicly claimable.
|
||||||
- **Once productive** (`setup: false`), normal service: the trusted proxy's **`X-Forwarded-Proto`**
|
- **Once productive** (`setup: false` with an admin), normal service: the trusted proxy's
|
||||||
drives `Secure` on the session cookie, the **`__Host-`** prefix, and HSTS (§6 · D-043).
|
**`X-Forwarded-Proto`** drives `Secure` on the session cookie, the **`__Host-`** prefix, and HSTS
|
||||||
|
(§6 · D-043). The XFF handling keeps the **client IP** honest (A6) — it is no longer a gate.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -148,14 +150,14 @@ type RequestContext struct {
|
||||||
| AC-4 | A session whose `user_id` ≠ the GUID's user is **rejected** |
|
| AC-4 | A session whose `user_id` ≠ the GUID's user is **rejected** |
|
||||||
| AC-5 | An **empty / missing session id** is rejected on **every** path (A2) |
|
| AC-5 | An **empty / missing session id** is rejected on **every** path (A2) |
|
||||||
| AC-6 | A `do`/`action` request without a valid CSRF token is **rejected** |
|
| AC-6 | A `do`/`action` request without a valid CSRF token is **rejected** |
|
||||||
| AC-7 | In setup mode, a request **relayed by a proxy** (a loopback peer **with** `X-Forwarded-For`) never reaches the **wizard** — it gets the first-run boilerplate (`/` · `/setup`) or a 404; the bootstrap is never first-visitor-wins (D-025 · D-044) |
|
| AC-7 | While the wizard is enabled (`setup: true`) with a **required `setup_code`**, `GET /setup` shows the **code form** to every peer; only an **unlocked** request reaches the wizard. The peer/tunnel rule of D-025 is retired (D-045) |
|
||||||
| AC-8 | Every response carries a correlation id, echoed in the log; path credentials are redacted |
|
| AC-8 | Every response carries a correlation id, echoed in the log; path credentials are redacted |
|
||||||
| AC-9 | The rate limiter returns **429** on the auth-sensitive routes (A3) |
|
| AC-9 | The rate limiter returns **429** on the auth-sensitive routes (A3) |
|
||||||
| AC-10 | In setup mode a request whose **TCP peer is not loopback**, **or** which carries **any `X-Forwarded-For`**, never reaches the wizard unless it is **unlocked with the setup code** (D-025/H2/A13/D-044) |
|
| AC-10 | While the wizard is enabled with a **required code**, no peer reaches the **wizard** without the unlock; the code form is shown instead, and every non-wizard route is 404 (D-045, superseding D-025/H2) |
|
||||||
| AC-11 | The **`setup` module** is reachable only through `Workflow == "setup"`; any other resolution naming it is 404 (M1c/L3s) |
|
| AC-11 | The **`setup` module** is reachable only through `Workflow == "setup"`; any other resolution naming it is 404 (M1c/L3s) |
|
||||||
| AC-12 | `X-Forwarded-For` is resolved **right→left**; a forged leftmost entry is ignored (H1/A6) |
|
| AC-12 | `X-Forwarded-For` is resolved **right→left**; a forged leftmost entry is ignored (H1/A6) |
|
||||||
| AC-13 | Behind the proxy, `X-Forwarded-Proto: https` ⇒ the session cookie is **`Secure` + `__Host-`**, and HSTS is set (D-043) |
|
| AC-13 | Behind the proxy, `X-Forwarded-Proto: https` ⇒ the session cookie is **`Secure` + `__Host-`**, and HSTS is set (D-043) |
|
||||||
| AC-14 | In setup mode `GET /` and `GET /setup` show the **first-run boilerplate** (never a bare 404); the **correct setup code unlocks the wizard**; a wrong/absent code does not (D-044) |
|
| AC-14 | `setup: true` → `GET /setup` serves the wizard (or the code form when `setup_code` is set); `GET /` points at it; the **correct setup code unlocks** it, a wrong/absent one does not (D-044/D-045). `setup: false` with no admin → a locked boilerplate at `/` and `/setup` (never a bare 404) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -211,6 +213,9 @@ The fixes touching this spec:
|
||||||
|
|
||||||
## 12. D-044 — the setup code gate & the first-run boilerplate (2026-10-15b)
|
## 12. D-044 — the setup code gate & the first-run boilerplate (2026-10-15b)
|
||||||
|
|
||||||
|
> **Superseded in part by D-045 (§13).** The **code mechanics** below stand; the **tunnel gate (a)**
|
||||||
|
> and the **generated** code are retired, and `GET /` is no longer the code form.
|
||||||
|
|
||||||
The topology (**LXC behind Proxmox + Zoraxy**) makes the SSH tunnel impractical: `pct enter`/SSH
|
The topology (**LXC behind Proxmox + Zoraxy**) makes the SSH tunnel impractical: `pct enter`/SSH
|
||||||
lands on the container **gateway**, so a tunnel to `127.0.0.1:8080` cannot be formed and gate (a)
|
lands on the container **gateway**, so a tunnel to `127.0.0.1:8080` cannot be formed and gate (a)
|
||||||
alone would lock the operator out. **D-025 is re-revised** so `/setup` is served when **either**
|
alone would lock the operator out. **D-025 is re-revised** so `/setup` is served when **either**
|
||||||
|
|
@ -229,3 +234,21 @@ alone would lock the operator out. **D-025 is re-revised** so `/setup` is served
|
||||||
|
|
||||||
AC-7/AC-10 are reworded: "refused" now means "refused the **wizard**" — the boilerplate may still
|
AC-7/AC-10 are reworded: "refused" now means "refused the **wizard**" — the boilerplate may still
|
||||||
answer `GET /`/`GET /setup` with 200 (AC-14).
|
answer `GET /`/`GET /setup` with 200 (AC-14).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. D-045 — the toggle gate, the optional code, the retired tunnel (2026-10-15c)
|
||||||
|
|
||||||
|
The **peer/tunnel gate is retired**: `setup: true` is the gate. `dispatch` evaluates the config
|
||||||
|
toggle per request:
|
||||||
|
|
||||||
|
- **`setup: true`** — `setupAdmitted(r)` is true when no `setup_code` is required, else it needs a
|
||||||
|
valid `SetupGate.Unlocked(r)`. `/setup` with a required code un-entered → `SetupGate.CodeForm`;
|
||||||
|
`GET /` → `SetupGate.Landing`; the **unlock action** is the one carve-out; every other route → 404
|
||||||
|
(static assets are served so the first-run pages render inside the shell).
|
||||||
|
- **`setup: false`, no admin** — the **LOCKED** state (`Deps.LockedMode`): `GET /` and `GET /setup`
|
||||||
|
→ `SetupGate.Locked`; every other route → 404.
|
||||||
|
|
||||||
|
Removed: `httpx.SetupRequestAllowed`, `httpx.IsLoopbackIP` (and their tests). Kept: the XFF
|
||||||
|
right→left client-IP resolution and the `X-Forwarded-Proto`-driven `Secure`/`__Host-`/HSTS (A6/A11
|
||||||
|
· D-043). `SetupGate` is now `{ Required · Unlocked · CodeForm · Landing · Locked }`.
|
||||||
|
|
|
||||||
|
|
@ -2,62 +2,69 @@
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Status** | **Implemented** (2026-10-13) — see §7; `build`/`vet`/`test`/`-race` green · static binary · live-smoked · **rev 2026-10-14 (D-041): TOTP-only setup** · **rev 2026-10-15 (D-025/D-043): per-request tunnel gate** · **rev 2026-10-15b (D-044): the setup code + the first-run boilerplate** — see §9 |
|
| **Status** | **Implemented** (2026-10-13) — see §7; `build`/`vet`/`test`/`-race` green · static binary · live-smoked · **rev 2026-10-14 (D-041): TOTP-only setup** · **rev 2026-10-15 (D-025/D-043): per-request tunnel gate — superseded** · **rev 2026-10-15b (D-044): the setup code + the first-run boilerplate** · **rev 2026-10-15c (D-045): the gate is the `setup` toggle; the code is optional; the tunnel gate retired** — see §9/§10 |
|
||||||
| **Created** | 2026-10-13 |
|
| **Created** | 2026-10-13 |
|
||||||
| **Author** | OpenArchitect |
|
| **Author** | OpenArchitect |
|
||||||
| **Target milestone** | **M0** |
|
| **Target milestone** | **M0** |
|
||||||
| **Governing decisions** | **D-015** · **D-021** · **D-025** · **D-026** · **D-035** · `PROJECT_PLAN.md` §5 |
|
| **Governing decisions** | **D-021** · **D-026** · **D-035** · **D-044** · **D-045** · `PROJECT_PLAN.md` §5 |
|
||||||
| **Related** | SPEC-003 (door) · SPEC-004 (identity) |
|
| **Related** | SPEC-003 (door) · SPEC-004 (identity) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Goal
|
## 1. Goal
|
||||||
|
|
||||||
Bring a fresh instance to life with a **`/setup` wizard** reachable **only over the direct SSH
|
Bring a fresh instance to life with a **`/setup` wizard** — enabled by the **`setup` config toggle**
|
||||||
tunnel** that creates the **first user = the top admin** (`user_id = 1`, role `admin`), then switches
|
(D-045) and optionally locked by a **setup code** (D-044) — that creates the **first user = the top
|
||||||
the instance to productive mode.
|
admin** (`user_id = 1`, role `admin`), then switches the instance to productive mode.
|
||||||
|
|
||||||
## 2. Non-goals
|
## 2. Non-goals
|
||||||
|
|
||||||
Module setup screens (each module owns its config). Multi-admin (there is **one**, D-034/§7.1).
|
Module setup screens (each module owns its config). Multi-admin (there is **one**, D-034/§7.1).
|
||||||
|
|
||||||
## 3. Setup mode
|
## 3. Setup mode & the gate (D-045)
|
||||||
|
|
||||||
`setup mode = (config.setup == true) OR (no user exists)` — **fail-safe**.
|
The gate is the **config toggle `setup`**. The instance is in one of **three** states:
|
||||||
|
|
||||||
- The app **may bind any address** — including `0.0.0.0:8080` behind a reverse proxy (D-043); there is
|
```
|
||||||
**no** bind refusal.
|
setup: false · no admin → LOCKED (a "set setup: true and restart" boilerplate; not claimable)
|
||||||
- **Gated — never first-visitor-wins (D-025 · D-044).** `/setup` is served to a request that is
|
setup: true → the WIZARD (or the code form when `setup_code` is set)
|
||||||
**either** **(a)** a **direct tunnel** — **TCP peer loopback** **and** **no `X-Forwarded-For`** (an
|
setup: false · admin → PRODUCTIVE
|
||||||
empty/unparseable peer fails closed; a same-host proxy presents a loopback peer, so the *absence of
|
```
|
||||||
XFF* is what proves no proxy) — **or** **(b)** **unlocked with the setup code** (below).
|
|
||||||
- **The first-run boilerplate (D-044).** In setup mode:
|
- The app **may bind any address** — `0.0.0.0:8080` behind the proxy (D-043); **no** bind refusal.
|
||||||
- `GET /` and `GET /setup` show a **friendly first-run page** — *"MyDay is not set up yet"* — with a
|
- **The gate is the config toggle `setup` (D-045).**
|
||||||
**setup-code entry form** and the instruction to read the code from the **server log**.
|
- **`setup: false`, no admin** → the app is **locked**: `GET /` and `GET /setup` show a
|
||||||
**Never a bare 404** (the domain must not look "down").
|
**boilerplate** — *"MyDay is not set up. Set `setup: true` in `config.yaml` and restart."*
|
||||||
- Every **other** route → **404**.
|
Every other route → **404**. **This state is not publicly claimable.**
|
||||||
- The correct code **unlocks the wizard**; the code is **single-use**, **rate-limited**, and
|
- **`setup: true`** → `/setup` serves the **wizard** (the operator's deliberate switch).
|
||||||
**redacted from the logs** (A8).
|
`GET /` → a friendly page pointing at `/setup`. Every other route → 404.
|
||||||
|
- **Completion** → the app writes **`setup: false`** and creates the admin → productive.
|
||||||
|
- **`setup_code` is OPTIONAL (D-044 · D-045).** If set in `config.yaml`, the wizard requires it (a
|
||||||
|
**second factor**) and the code form comes first; if empty, the toggle alone admits. Single-use;
|
||||||
|
rate-limited; **redacted** (A8).
|
||||||
|
- The **peer/tunnel gate (D-025) is retired** — `setup: true` is the gate; the `SetupRequestAllowed`
|
||||||
|
peer+`X-Forwarded-For` rule no longer decides setup access. The XFF/XFP handling is **kept** for
|
||||||
|
the **client IP** and the `Secure`/`__Host-` cookies + HSTS (A6/A11/D-043).
|
||||||
- The `setup` **module** is pinned at the door: the wizard's actions are reachable only through the
|
- The `setup` **module** is pinned at the door: the wizard's actions are reachable only through the
|
||||||
reserved `setup` workflow (M1c/L3s · SPEC-003 §4).
|
reserved `setup` workflow (M1c/L3s · SPEC-003 §4).
|
||||||
|
- **Residual (D-045):** while `setup: true`, whoever reaches `/setup` first can claim the admin —
|
||||||
|
set `setup_code`, or complete setup immediately.
|
||||||
|
|
||||||
## 4. The wizard
|
## 4. The wizard
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /setup → the first-run boilerplate + the setup-code form
|
# setup: false · no admin → the boilerplate ("enable setup") ← locked, not claimable
|
||||||
POST /setup?do=setup&action=unlock → the code → unlocks the wizard (a session flag)
|
# setup: true → the wizard
|
||||||
GET /setup → the wizard (once unlocked, or via the tunnel)
|
GET /setup → (if `setup_code` is set) the code form, else the wizard
|
||||||
|
POST /setup?do=setup&action=unlock → the code → unlocks (only when `setup_code` is set)
|
||||||
Step 1 domain / base_url
|
Step 1 domain / base_url
|
||||||
Step 2 the top admin — display_name · email · **TOTP enrollment** (Passkey deferred — D-041)
|
Step 2 the top admin — display_name · email · **TOTP enrollment** (Passkey deferred — D-041)
|
||||||
Step 3 mail (SMTP) · locale · timezone
|
Step 3 mail (SMTP) · locale · timezone
|
||||||
POST /setup?do=setup&action=… → each step (CSRF-protected)
|
POST /setup?do=setup&action=… → each step (CSRF-protected)
|
||||||
Finish → persist settings → config.setup = false
|
Finish → persist settings → **setup: false**
|
||||||
→ user_id = 1 (role admin) · identity minted + activated · session minted
|
→ user_id = 1 (role admin) · identity minted + activated · session minted
|
||||||
```
|
```
|
||||||
|
|
||||||
- **The setup code (D-044):** a **128-bit** value generated at first run and printed in a **startup
|
|
||||||
banner** (`setup.code` in config overrides it). A direct-tunnel request (gate **a**) needs no code.
|
|
||||||
|
|
||||||
- **Settings persist to `config.yaml`** (D-035). *Caveat:* if the file is not writable, the wizard
|
- **Settings persist to `config.yaml`** (D-035). *Caveat:* if the file is not writable, the wizard
|
||||||
**prints the values** and instructs the operator to write them, then marks setup complete.
|
**prints the values** and instructs the operator to write them, then marks setup complete.
|
||||||
- **One admin only:** the wizard creates `user_id = 1`; later attempts to create another `admin`
|
- **One admin only:** the wizard creates `user_id = 1`; later attempts to create another `admin`
|
||||||
|
|
@ -69,6 +76,9 @@ POST /setup?do=setup&action=… → each step (CSRF-protected)
|
||||||
## 5. After setup
|
## 5. After setup
|
||||||
|
|
||||||
- `/setup` → **404** (delete, don't redirect). Re-entering requires `config.setup: true` + restart.
|
- `/setup` → **404** (delete, don't redirect). Re-entering requires `config.setup: true` + restart.
|
||||||
|
- `config.yaml` now reads **`setup: false`**; with an admin present the instance is **productive**.
|
||||||
|
(Without an admin it would be the **locked** state — the toggle is off and there is nothing to
|
||||||
|
serve; set `setup: true` and restart to resume.)
|
||||||
- The instance is served on its configured bind (typically `0.0.0.0:8080` behind the reverse proxy,
|
- The instance is served on its configured bind (typically `0.0.0.0:8080` behind the reverse proxy,
|
||||||
D-043); the request scheme is taken from `X-Forwarded-Proto` (trusted proxy) or TLS.
|
D-043); the request scheme is taken from `X-Forwarded-Proto` (trusted proxy) or TLS.
|
||||||
- **Audit:** the bootstrap, the admin creation, and the enroll are all recorded (A11).
|
- **Audit:** the bootstrap, the admin creation, and the enroll are all recorded (A11).
|
||||||
|
|
@ -77,13 +87,14 @@ POST /setup?do=setup&action=… → each step (CSRF-protected)
|
||||||
|
|
||||||
| # | Criterion |
|
| # | Criterion |
|
||||||
|---|---|
|
|---|---|
|
||||||
| AC-1 | Fresh instance → `GET /` and `GET /setup` show the **first-run boilerplate** (200 — **never a bare 404**); every **other** route → 404 |
|
| AC-1 | `setup: true` → `GET /setup` serves the **wizard** directly when `setup_code` is empty; `GET /` points at it; every **other** route → 404 |
|
||||||
| AC-2 | In setup mode the **wizard** is served to **either** the direct tunnel (loopback TCP peer **and** no `X-Forwarded-For`) **or** a request **unlocked with the setup code**; anything else gets the boilerplate (`/` · `/setup`) or a 404. Any bind is allowed (D-025 revised) |
|
| AC-2 | With `setup_code` **set**, `GET /setup` shows the **code form** first (from any peer); only a request **unlocked** with the code reaches the wizard; a wrong/absent code gets a generic 403. The peer/tunnel rule (D-025) is retired (D-045) |
|
||||||
| AC-3 | Completing the wizard creates `user_id = 1` (role `admin`) with **TOTP** enrolled and **no passkey** (D-041) |
|
| AC-3 | Completing the wizard creates `user_id = 1` (role `admin`) with **TOTP** enrolled and **no passkey** (D-041) |
|
||||||
| AC-4 | After completion `config.setup = false` (or printed) and `/setup` → 404 |
|
| AC-4 | After completion `config.setup = false` (or printed) and `/setup` → 404 |
|
||||||
| AC-5 | A second `admin` cannot be created |
|
| AC-5 | A second `admin` cannot be created |
|
||||||
| AC-6 | The full bootstrap is audited |
|
| AC-6 | The full bootstrap is audited |
|
||||||
| AC-7 | The setup code is **single-use** and **rate-limited**; a wrong or absent code gets a **generic** answer that reveals nothing, and the code is **redacted from the logs** (A8). The direct tunnel needs no code (D-044) |
|
| AC-7 | The setup code (when set) is **single-use** and **rate-limited**; a wrong or absent code gets a **generic** answer that reveals nothing, and the code is **redacted from the logs** (A8) |
|
||||||
|
| AC-8 | `setup: false` with **no admin** → **LOCKED**: `GET /` and `GET /setup` render the "enable setup" boilerplate (200 — **never a bare 404**), every **other** route → 404, and the wizard is **not claimable** (D-045) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -118,7 +129,7 @@ every GUID branch. *(Delete, don't redirect — a fresh instance has no app yet.
|
||||||
| D-1 | **One admin only** enforced by `users.Create`; the wizard returns **409** on a second attempt. | **Accepted** (AC-5). |
|
| D-1 | **One admin only** enforced by `users.Create`; the wizard returns **409** on a second attempt. | **Accepted** (AC-5). |
|
||||||
| D-2 | The wizard's step is **derived** from the DB (admin exists? identity pending/active?) plus an in-memory *draft* for step 1's domain. | **Accepted** as the smallest state model. |
|
| D-2 | The wizard's step is **derived** from the DB (admin exists? identity pending/active?) plus an in-memory *draft* for step 1's domain. | **Accepted** as the smallest state model. |
|
||||||
| D-3 | A not-writable `config.yaml` → the YAML is **printed** to stdout with an instruction; setup still completes in memory. | **Accepted** (AC-4; §4 caveat). |
|
| D-3 | A not-writable `config.yaml` → the YAML is **printed** to stdout with an instruction; setup still completes in memory. | **Accepted** (AC-4; §4 caveat). |
|
||||||
| D-4 | **Setup mode is LATCHED at startup** (`config.setup` OR "not yet bootstrapped" — no user, or no ACTIVE admin) and cleared by an atomic `Actions.Done()`; `config.Setup` is never mutated (the live `Config` is an immutable startup snapshot, D-035). | **Accepted** — avoids a data race on the shared config and a per-request DB query, and **holds setup mode through the whole wizard**: once the admin row exists a live "no user" check would end setup mode before enrollment/finish. |
|
| D-4 | **Setup mode is LATCHED at startup** (`config.Setup`, D-045) and cleared by an atomic `Actions.Done()`; `config.Setup` is never mutated (the live `Config` is an immutable startup snapshot, D-035). | **Accepted** — avoids a data race on the shared config and a per-request DB query, and **holds setup mode through the whole wizard**: once the admin row exists a live "no user" check would end setup mode before enrollment/finish. *(D-045): the "no user" clause moved to the LOCKED state; setup mode is now purely the toggle.* |
|
||||||
|
|
||||||
**Architect rulings on the flagged items:**
|
**Architect rulings on the flagged items:**
|
||||||
|
|
||||||
|
|
@ -155,11 +166,16 @@ The admin adds a passkey at first login on the real origin, via the **passkey nu
|
||||||
|
|
||||||
## 9. Implementation notes — the setup code & boilerplate (D-044, 2026-10-15b)
|
## 9. Implementation notes — the setup code & boilerplate (D-044, 2026-10-15b)
|
||||||
|
|
||||||
|
> **Superseded in part by D-045 (§10).** The D-044 *mechanics* (the signed unlock cookie, the
|
||||||
|
> single-use/rate-limited/redacted code, the generic 403) are unchanged; what changed is the
|
||||||
|
> **gate**: `setup: true` is now the gate, `setup_code` is **optional**, and the **peer/tunnel gate
|
||||||
|
> is retired**. The code-form page now lives only at `GET /setup` (not `/`), and the code is no
|
||||||
|
> longer generated when blank.
|
||||||
|
|
||||||
Built 2026-10-15. `build` · `vet` · `test -race` green; **static CGO-free** binary; live-smoked
|
Built 2026-10-15. `build` · `vet` · `test -race` green; **static CGO-free** binary; live-smoked
|
||||||
against the real binary (banner + code, boilerplate at `/` and `/setup` behind a proxy, the tunnel
|
against the real binary (banner + code, boilerplate at `/` and `/setup` behind a proxy, wrong code →
|
||||||
opens the wizard with no code, wrong code → **403**, correct code → **303** + unlock cookie, the
|
**403**, correct code → **303** + unlock cookie, the wizard follows, a spent code is refused, and
|
||||||
wizard follows, a spent code is refused, and neither the code nor a query-borne copy appears in
|
neither the code nor a query-borne copy appears in the access log).
|
||||||
the access log).
|
|
||||||
|
|
||||||
**What landed**
|
**What landed**
|
||||||
|
|
||||||
|
|
@ -178,7 +194,7 @@ the access log).
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **The code** | 128-bit, generated at startup (`crypto/rand`), printed in the banner, or pinned by `setup_code`. Entry ignores case/separators. |
|
| **The code** | 128-bit, **pinned by `setup_code`** (D-045: no code is generated when it is blank — the toggle alone admits). Entry ignores case/separators. |
|
||||||
| **Single-use** | `Accept` retires the code on the **first success**; a failed attempt does not consume it. |
|
| **Single-use** | `Accept` retires the code on the **first success**; a failed attempt does not consume it. |
|
||||||
| **The unlock** | A signed cookie (`myday_setup`) carrying a **fingerprint** of the code — not the code — so it survives the code's retirement but dies on a restart (new code) or on completion (`Clear`). It is **not** the session cookie (no user exists). |
|
| **The unlock** | A signed cookie (`myday_setup`) carrying a **fingerprint** of the code — not the code — so it survives the code's retirement but dies on a restart (new code) or on completion (`Clear`). It is **not** the session cookie (no user exists). |
|
||||||
| **Generic** | A wrong and an absent code return the same **403** page/message. |
|
| **Generic** | A wrong and an absent code return the same **403** page/message. |
|
||||||
|
|
@ -189,7 +205,40 @@ the access log).
|
||||||
| # | Builder choice | Ruling |
|
| # | Builder choice | Ruling |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **D-1** | The config key is **`setup_code`** (top level), not the literal `setup.code`: `setup:` is already the boolean mode flag (D-026), and YAML cannot be both a scalar and a mapping. | **Accepted** — the smallest faithful encoding of "`setup.code` in config". |
|
| **D-1** | The config key is **`setup_code`** (top level), not the literal `setup.code`: `setup:` is already the boolean mode flag (D-026), and YAML cannot be both a scalar and a mapping. | **Accepted** — the smallest faithful encoding of "`setup.code` in config". |
|
||||||
| **D-2** | "Single-use" = the code retires on the first **successful** unlock; the browser keeps its signed unlock cookie (multi-tab works), a **second** visitor cannot re-use the printed value. A restart prints a **fresh** code. | **Accepted** — honours single-use without a lock-out when a tab is closed. |
|
| **D-2** | "Single-use" = the code retires on the first **successful** unlock; the browser keeps its signed unlock cookie (multi-tab works), a **second** visitor cannot re-use the printed value. A restart re-arms a pinned `setup_code`. | **Accepted** — honours single-use without a lock-out when a tab is closed. |
|
||||||
| **D-3** | `GET /` shows the boilerplate **always** in setup mode (tunnel or not) — `/` is not the wizard. | **Accepted.** |
|
| **D-3** | `GET /` shows the first-run page **always** while the wizard is enabled — `/` is not the wizard. *(D-045): `/` is now a landing that points at `/setup`; the code form is at `/setup`.* | **Accepted.** |
|
||||||
| **D-4** | With **no** gate wired (a door built by hand) setup mode keeps the strict 404 — the entrypoint always wires it. | **Accepted** — keeps the door honest without a setup package. |
|
| **D-4** | With **no** gate wired (a door built by hand) the first-run pages fall back to the strict 404 — the entrypoint always wires it. | **Accepted** — keeps the door honest without a setup package. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Implementation notes — the toggle gate & the optional code (D-045, 2026-10-15c)
|
||||||
|
|
||||||
|
Built 2026-10-15. `build` · `vet` · `test -race` green; **static CGO-free** binary; live-smoked
|
||||||
|
against the real binary across all three states (locked boilerplate at `/` and `/setup`, the
|
||||||
|
code-less wizard, the code form → unlock → wizard, and a full bootstrap: `domain.save` →
|
||||||
|
`admin.create` → `admin.totp.begin/verify` → `finish` → `config.yaml` reads `setup: false` and
|
||||||
|
`/setup` → 404).
|
||||||
|
|
||||||
|
**What changed**
|
||||||
|
|
||||||
|
- **The gate is `config.setup`** — `setup: true` enables the wizard; `setup: false` with **no admin**
|
||||||
|
is the **LOCKED** state. The `!bootstrapped()` fail-safe clause was removed from setup mode and
|
||||||
|
became the locked predicate (latched at startup).
|
||||||
|
- **`setup_code` is optional** — `setup.New` no longer generates a code; a blank code makes
|
||||||
|
`Gate.Required()` false and the toggle alone admits. When set, the D-044 code mechanics apply
|
||||||
|
unchanged.
|
||||||
|
- **The peer/tunnel gate is retired** — `httpx.SetupRequestAllowed` (and `httpx.IsLoopbackIP`) are
|
||||||
|
**deleted**; the XFF/XFP handling is **kept** for the client IP and the `Secure`/`__Host-` cookies
|
||||||
|
+ HSTS (A6/A11/D-043).
|
||||||
|
- **Three first-run pages** — `Gate.CodeForm` (setup + required code, un-entered), `Gate.Landing`
|
||||||
|
(`GET /` while setup is enabled), `Gate.Locked` (setup off, no admin). `Deps.LockedMode` carries
|
||||||
|
the locked predicate; `Deps.SetupMode` now means "the wizard is enabled".
|
||||||
|
- **The startup banner** states the mode; it prints the code **only when `setup_code` is set**; in
|
||||||
|
the locked state it tells the operator to set `setup: true` and restart.
|
||||||
|
|
||||||
|
| # | Builder choice | Ruling |
|
||||||
|
|---|---|---|
|
||||||
|
| **D-1** | Static assets stay served in **every** first-run state (locked + enabled) so the boilerplate/code form render inside the shell — the "every other route → 404" rule covers app routes. | *(builder; the smallest faithful reading — a stylesheet for the page that replaces the 404)* |
|
||||||
|
| **D-2** | "No admin" (for the locked predicate) = **no user row at all**, latched at startup — not "no ACTIVE admin". A pending admin is therefore *productive* (enrolls via `/{GUID}`), never locked. | *(builder)* |
|
||||||
|
| **D-3** | The `unlock` action when no code is required is a no-op **303 → /setup** rather than a 404. | *(builder — a stale bookmark works)* |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ log) rather than silently losing precedence (SPEC-008 §9·D-9).
|
||||||
|
|
||||||
| # | Criterion |
|
| # | Criterion |
|
||||||
|---|---|
|
|---|---|
|
||||||
| AC-1 | A human completes **`/setup`** in a browser (over the tunnel) and reaches the dashboard |
|
| AC-1 | A human completes **`/setup`** in a browser (while the wizard is enabled, D-045) and reaches the dashboard |
|
||||||
| AC-2 | A **TOTP login** works in a browser; the **nudge** appears (non-loopback); **adding a passkey** succeeds; a later **passkey login** works |
|
| AC-2 | A **TOTP login** works in a browser; the **nudge** appears (non-loopback); **adding a passkey** succeeds; a later **passkey login** works |
|
||||||
| AC-3 | The page emits **no inline scripts** (the CSP `script-src 'self'` is not weakened) |
|
| AC-3 | The page emits **no inline scripts** (the CSP `script-src 'self'` is not weakened) |
|
||||||
| AC-4 | The **manifest** is valid and the app **installs**; `start_url` is `/` |
|
| AC-4 | The **manifest** is valid and the app **installs**; `start_url` is `/` |
|
||||||
|
|
@ -189,4 +189,4 @@ worker's `isCacheable` in Node (skipped when Node is absent) would harden AC-5 f
|
||||||
| **D-6** | The nudge's passkey button is **JS-only**. | **Accepted** — the ceremony *is* the request body; `later`/`never` keep plain forms + `<noscript>`. |
|
| **D-6** | The nudge's passkey button is **JS-only**. | **Accepted** — the ceremony *is* the request body; `later`/`never` keep plain forms + `<noscript>`. |
|
||||||
| **Q-1** | A discovered module may be named for a reserved **level-1** workflow (`static` · `access` · …). | **Resolved — extend the guard** to those names, **except `home`** (a legitimate module). A colliding module fails to load (error + log). *Fold into the next slice; O-15 resolved.* |
|
| **Q-1** | A discovered module may be named for a reserved **level-1** workflow (`static` · `access` · …). | **Resolved — extend the guard** to those names, **except `home`** (a legitimate module). A colliding module fails to load (error + log). *Fold into the next slice; O-15 resolved.* |
|
||||||
| **Q-2** | AC-5's service-worker JS is not *executed* by the suite. | **Accepted as-is** — the Go policy is the single testable statement of the rule. Running `isCacheable` under Node (skipped when absent) is optional hardening, later. |
|
| **Q-2** | AC-5's service-worker JS is not *executed* by the suite. | **Accepted as-is** — the Go policy is the single testable statement of the rule. Running `isCacheable` under Node (skipped when absent) is optional hardening, later. |
|
||||||
| **Q-3** | AC-1/AC-2 need a browser; the tunnel must target `localhost`. | **Accepted** — the README gains the caveat: with an `https://` base URL the cookies are `Secure`, so reach the app at **`localhost`/`127.0.0.1`** (a trustworthy origin). *Fold into the next slice.* |
|
| **Q-3** | AC-1/AC-2 need a browser. | **Accepted** — the README notes the WebAuthn caveat: the passkey is enrolled at the app's real origin (a trustworthy https/`localhost` context), and with an `https://` base URL the cookies are `Secure`. *Fold into the next slice.* |
|
||||||
Loading…
Reference in a new issue