diff --git a/CHANGELOG.md b/CHANGELOG.md index a96c456..27ec86b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## Sign-up locked twice (the app's own switch + a case-insensitive block); wanderer closable; three more probes; a probe must carry its measurement (2026-09-29 evening, decisions 47–49) + +- **The app's own sign-up switch** on adventurelog, calcom, gitea, gramps-web, homebox, papra, sparkyfitness, termix, + vikunja: the compose reads `SIGNUP_CLOSED` / `SIGNUP_OPEN` with an OPEN default (installed apps unchanged), and + `after_setup:` closes it when the gate opens (controller ≥ 0.282.0). Measured: 6 of the 9 switches also refuse the + household's own first account, so none is set at install. Straight at the app, past the block, each refuses a stranger. +- **Every `signup_block` is now case-insensitive and slash-tolerant** (`PathRegexp((?i)…)`). Measured: termix's router + ignores case — `/users/CREATE` reached its sign-up past the old block (its own switch refused it). Final trick run: + 113 tries on 11 apps, 0 got in, 106 refused by the block. +- **wanderer (decision 48):** not gated (its web server calls its own database through the public name); sign-up closed + by the household's "Close sign-up now" — the block covers `/register` and PocketBase's `POST + /api/collections/(users|_pb_users_auth_)/records` in any case (a stranger got in by the collection id and by `USERS` + before the fix); `PUBLIC_DISABLE_SIGNUP` follows `SIGNUP_CLOSED`. First step reworded (hu + en). +- **Probes (R-715):** ghost `setup.0.status`, home-assistant `0.done`, gramps-web `done_status: 405` — each measured before + and after on a fresh install; the press before the setup was refused on all three. +- **New gate `check-probe-measured.py`** (in `catalog_gates.py`, `--fast`): a `setup_done_probe:` needs "measured", a date + and both answers directly above it; 5 decoys. immich/n8n/audiobookshelf got their note moved there. +- `test_gate_decoys.py`: the docmost fixture read its PostgreSQL major instead of typing 16 — it had stopped every run + since docmost moved to 18 ("fixture drifted"). 97 cases pass. + ## The setup gate on 28 more apps; open sign-up closed after the first admin; claper's password never in code (2026-09-29 afternoon, decisions 46–47) - **`setup_gate: true` on 28 more class-4 apps** (32 of 34; controller ≥ 0.281.0). Probes measured before and after diff --git a/FIRST-ADMIN.md b/FIRST-ADMIN.md index ed7779f..df80bb3 100644 --- a/FIRST-ADMIN.md +++ b/FIRST-ADMIN.md @@ -81,10 +81,36 @@ asks the probe first where there is one. Open sign-up is closed by the box after | **uptime-kuma** | 4 | first visitor creates admin (socket.io `setup`) | **setup gate**, no HTTP probe → the household's „Kész, beállítottam" | **GATED** — catalog, 2026-09-29 | **M 9202**: gate held across a controller restart; the press opened it (second press 409). The proof pressed WITHOUT doing the socket.io setup — the press trusts the household | | vaultwarden | 1 | `ADMIN_TOKEN` generated; invite-only (R-512) | – | fine | R | | **vikunja** | 4 | open registration | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); sign-up closed by the box after the setup: `/api/v1/register` | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | -| wanderer | 4 | `PUBLIC_DISABLE_SIGNUP=false` | NOT gated: its web part calls its own database host through the public name, which a gate would refuse (the household could not finish the setup); open sign-up (`PUBLIC_DISABLE_SIGNUP=false`) and PocketBase's own first-run screen on the second host — R-714 | open — **needs a design**; its first step says plainly that anyone who finds the address can make an account (decision 47) | R; measured the call on 9202 | +| **wanderer** | 4 | `PUBLIC_DISABLE_SIGNUP=false` | NOT gated (its web server calls its own database through the public name); no first-admin screen to take (PocketBase's superuser installer needs the one-time link from its log); sign-up closed by the household's „Close sign-up now” after its account (decision 48) | **LOCKED on the household's press** — catalog, 2026-09-29 evening | **M 9202**: before, a stranger made an account through PocketBase; after the press, every sign-up shape refused (`felhom.eu/documentation/audits/signup-lock-2026-09-29/D/`) | | **wger** | 3 | `admin / adminadmin` | (b) Django `set_password`, password as `sys.argv[1]` | **FIXED** — catalog, 2026-09-29; + R-712 (a browser's https Origin was refused by CSRF) | **M 9202**: fresh install — default refused, generated signs in (302), wrong refused, with the browser's https Origin | | **wishlist** | 4 | first sign-up is admin | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); sign-up closed by the box after the setup: `/signup` | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | | **zipline** | 4 | v4 sets up on first run; the stale `admin / zipline` note is **removed** (2026-09-29) | **setup gate**, opened by probe `/api/server/public` → `firstSetup` = false; the app itself refuses a stranger's second first-admin / sign-up call | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | +## Sign-up locks after the first admin (decision 47; controller ≥ 0.282.0) — measured on 9202, 2026-09-29 evening + +Two locks per app where the app has its own switch: the app's own setting (set by `after_setup` when the gate opens) and +the address block (`signup_block`, case-insensitive). Tricks: each sign-up route tried in 8 shapes (as written, trailing +slash, UPPER, Mixed, last part UPPER, one letter percent-encoded, double slash, query string), through the public route. +Evidence: `felhom.eu/documentation/audits/signup-lock-2026-09-29/`. + +| app | its own switch (env) | that switch also refuses the household's FIRST account? | address block (routes) | tricks (final run) | +|---|---|---|---|---| +| adventurelog | `DISABLE_REGISTRATION` | yes | `/signup`, allauth API, `/accounts/signup` | all refused (block) | +| calcom | `NEXT_PUBLIC_DISABLE_SIGNUP` (read at runtime; invites with a token still work) | no | `/signup`, `/auth/signup`, `/api/auth/signup` | all refused | +| gitea | `GITEA__service__DISABLE_REGISTRATION` (env-to-ini at every start) | no (the installer makes the admin) | `/user/sign_up` | all refused | +| gramps-web | `GRAMPSWEB_REGISTRATION_DISABLED` | no (create_owner) | `/api/users//register/` | all refused | +| homebox | `HBOX_OPTIONS_ALLOW_REGISTRATION` | yes | `/api/v1/users/register` | all refused | +| papra | `AUTH_IS_REGISTRATION_ENABLED` | yes | `/api/auth/sign-up/*` | all refused | +| sparkyfitness | `SPARKY_FITNESS_DISABLE_SIGNUP` | yes | `/api/auth/sign-up/*` | all refused | +| termix | `ALLOW_REGISTRATION` | yes | `/users/create` — **its router ignores case**: `/users/CREATE` reached the sign-up before the block was made case-insensitive (its own switch refused it) | all refused | +| vikunja | `VIKUNJA_SERVICE_ENABLEREGISTRATION` | yes (its register route answers 404) | `/api/v1/register` | all refused (its `/register` PAGE is only HTML; the API is blocked) | +| opengist | none reachable (an admin setting in its database) | — | `/-/register` | all refused (block only) | +| wishlist | none reachable (`system_config.enableSignup` in its database) | — | `/signup` | all refused (block only) | +| wanderer | `PUBLIC_DISABLE_SIGNUP` (web only — PocketBase's API ignores it) | — | `/register` + PocketBase `POST /api/collections/(users\|_pb_users_auth_)/records`, any case — **the collection id and a case change both got in before the fix** | all refused; the batch API is off by the app | + +The household's 15-minute window lifts both locks (the app restarts once) and the loop puts both back (measured on +homebox). "Close sign-up now" (decision 49) is offered on an app installed before the rule — and on wanderer, which is +not gated: the household makes its account, then presses it (measured on 9202). + **Counts (computed from the table, 2026-09-29 afternoon):** class 1: 8 · class 2: 1 · class 3: 5 · class 4: 34 · class 5: 5. -**Fixed: 5.** **Gated: 32** (3 of them without a proven opening). **Not gated: 2** (wanderer — R-714; plant-it — not installable). Fine: 14. +**Fixed: 5.** **Gated: 32** (3 of them without a proven opening). **Not gated: 2** (wanderer — its sign-up is locked by the household's press instead, decision 48; plant-it — not installable). Fine: 14. diff --git a/README.md b/README.md index e7df9c2..809614f 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,20 @@ app_info: **Measure it on 9202:** after the setup a stranger's sign-up succeeds (the reason for the block), with the block it is refused, and the rest of the app still answers. +**Two locks since controller 0.282.0.** Where the app has its OWN sign-up switch, wire it to `SIGNUP_CLOSED` / +`SIGNUP_OPEN` with an OPEN default in the compose (`- DISABLE_REGISTRATION=${SIGNUP_CLOSED:-false}`) — so an app +installed before is unchanged by the catalog — and declare `after_setup: {env: {SIGNUP_CLOSED: "true"}}`. The box sets +it when the gate opens (or on "close sign-up now") and starts the app once. Measure whether the switch also blocks the +household's own first account (it does on 6 of 9); `after_setup` avoids that either way. + +**Write the block case-insensitive and slash-tolerant:** `PathRegexp(`(?i)^/+api/+v1/+register`)`. Measured +2026-09-29: termix's router ignores case (`/users/CREATE` reached its sign-up), and PocketBase takes a collection name in +any case and by its id. Test every block with `felhom.eu/documentation/audits/signup-lock-2026-09-29/B/tricks.py`. + +**`setup_done_probe:` must carry its measurement** in the comment directly above it — "measured", a date and both +answers (`false -> true`, or "before … after …"). Gate: `scripts/check-probe-measured.py`. A probe may index a list +(`setup.0.status`) and may count one non-200 status as done (`done_status: 405`, controller ≥ 0.282.0). + ### App-email mapping (`smtp_mapping`) Apps that can send outbound email (password resets, invites, confirmations) get it through diff --git a/REPORT.md b/REPORT.md index 8c8d875..4b4ee7e 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,10 +1,10 @@ -# REPORT — 2026-09-29 afternoon: the setup gate on 28 more apps; sign-up closed after the first admin (decisions 46–47) +# REPORT — 2026-09-29 evening: sign-up locked twice; wanderer closable; three probes; the probe-measured gate (decisions 47–49) -Architecture: `09-update-architecture.md` §3 decisions 46–47; `01-topology-and-trust.md` §5. Controller 0.281.0. -Evidence: `felhom.eu/documentation/audits/gate-rollout-2026-09-29/`. Per-app status: `FIRST-ADMIN.md`. +Architecture: `09-update-architecture.md` §3 decisions 47–49, `01-topology-and-trust.md` §5. Controller 0.282.0. +Evidence: `felhom.eu/documentation/audits/signup-lock-2026-09-29/`. Per-app table: `FIRST-ADMIN.md` (sign-up locks). -- 32 of 34 class-4 apps gated; 9 by a measured probe, the rest by the household's press (which asks the probe first). -- 11 apps get a sign-up block (measured open before, refused after); 11 more refuse a stranger by themselves. -- wanderer not gated (R-714); plant-it not installable; seerr/outline/rallly's opening not provable on 9202. -- claper: the password reaches its Elixir code base64-encoded (R-713), proven live with `"` and `#{`. -- Gates: pre-push catalog gates; copy freeze recaptured (diff = the 11 new sentences). +- 9 apps: the app's own switch closed by `after_setup` when the gate opens (measured: refuses a stranger straight at the app). +- All 11 blocks case-insensitive; final trick run 113 tries, 0 got in. +- wanderer: sign-up closed by "Close sign-up now"; two holes found and closed (collection id, letter case). +- ghost, home-assistant, gramps-web: probes measured; gates opened by themselves. +- Gates: new `probe-measured` (5 decoys); decoy suite fixed (docmost fixture) — 97 cases pass. diff --git a/REUSE.md b/REUSE.md index 6c0c143..60c32b1 100644 --- a/REUSE.md +++ b/REUSE.md @@ -24,6 +24,7 @@ Templates are config; the few script helpers other scripts must REUSE, never re- | **Known default login → `after_install:`** (decision 45, controller ≥ 0.279.0) | `templates/bookstack/.felhom.yml` (`ADMIN_PASSWORD` field + `after_install:` block); `FIRST-ADMIN.md` for every app | An app that starts with a known admin login gets a generated `type: password` field (`generate: "password:24"`, `locked_after_deploy: true`) and ONE `after_install: {service, env: [ADMIN_PASSWORD], command: [...], success: ""}` through the app's OWN CLI, run once after a FRESH install. Keep `app_info.default_creds` — the page hides it once the command succeeded and warns while it is in effect. **Prove on 9202 (drill catalog) before live: the default fails, the generated password works, a wrong one fails.** TRAPS: `success:` is required because a CLI can exit 0 on an error (claper's `rpc`); **pass the password as its own argument, never inside program code** (`sys.argv[1]` — mealie, wger; security review 2026-09-29); a special-character policy uses `generate: "password:24:special"` (controller ≥ 0.280.0, calibre-web); a Hungarian first-steps change needs `check-copy-i18n.py --capture-freeze`. | | **Open first-run screen → `setup_gate:`** (decision 46, controller ≥ 0.280.0) | `templates/n8n/.felhom.yml` (probe), `templates/uptime-kuma/.felhom.yml` (no probe → the household's button); `FIRST-ADMIN.md` | `setup_gate: true` + optional `setup_done_probe: {url: http://:/, field: , done: ""}` | TRAPS: the probe must FLIP on the setup — measure it before and after on 9202; an app with open sign-up after setup (R-711) is not closed by the gate; `url` is read on the docker network, so it names the container, not the subdomain. | | **Open sign-up after the setup → `signup_block:`** (decision 47, controller ≥ 0.281.0) | `templates/opengist/.felhom.yml`, `templates/calcom/.felhom.yml` | `signup_block: ""` + `app_info.add_people` (hu) / `i18n.en.app_info.add_people` | TRAPS: block the app's API sign-up call, not only the page; an app's own invite link often uses the same address (the household's 15-minute window covers it); `add_people` is copy — `--capture-freeze`. | +| **The app's own sign-up switch → `after_setup:`** (decisions 47/49, controller ≥ 0.282.0) | `templates/homebox/` (compose `HBOX_OPTIONS_ALLOW_REGISTRATION=${SIGNUP_OPEN:-true}` + `.felhom.yml` `after_setup.env`) | compose default OPEN; `after_setup: {env: {SIGNUP_CLOSED: "true"}}` | TRAPS: the default must be OPEN (an installed app is unchanged by the catalog); several apps' switch also refuses the household's FIRST account, so never set it at install; an app with no env switch (opengist, wishlist) keeps the block alone — make that block case-insensitive. | | Controller-side health probe | `templates/vaultwarden/.felhom.yml` (`healthcheck:` block) | `healthcheck.checks[]` with `type: http` (port only), `type: api` (port + `path` + `expect.status: 200`), or `type: tcp` (port only — mealie, crafty-controller). Prefer `api` with a real health path when the app has one. | | App lifecycle (`available`/`hidden`/`abandoned`) | `templates/plant-it/.felhom.yml` (`lifecycle:` block) | Optional top-level `lifecycle:` in `.felhom.yml`. Absent/empty ≡ `available`. `hidden` = not offered for new installs; `abandoned` = same, PLUS a permanent "Nem karbantartott" badge + notice on every box already running it. **Deployed instances keep full function in both states** — lifecycle governs what is OFFERED, never what runs; the controller refuses a deploy of a non-available template server-side (fail-closed, so a stale link or direct POST cannot install one). Unknown value → treated as `available` + one WARN, never a broken template. **Do NOT take an app out of circulation by deleting or moving its directory** — that orphans every customer already running it, which is what the 2026-07-21 `retired/` experiment got wrong. The resolvability gate skips non-available apps, so an abandoned app's dead image is not a standing red. | | **Catalog gates — THE entry point** | `scripts/catalog_gates.py` | **Run `python3 scripts/catalog_gates.py ` after ANY template change** (mandated in `CLAUDE.md`). Runs all four gates below in order — image-pins, image-resolvable, volume-persistence, engine-major (2026-09-13; git-history diff, hook-only until CI fetches deeper, R-452) — and exits **non-zero if any fails**; **2 (UNDETERMINED) is reported distinctly and is never a pass**, 1 (convicted) outranks 2 in the summary. Naming app(s) scopes the two gates that accept scoping, which is the normal after-a-change run; with no names the RUNTIME gate deploys every template, so that form is **scratch host only**. **Why a runner** (operator ruling 2026-08-02, R-161): the only gates in this project that ever get run are the ones with a single entry point named in a CLAUDE.md — `felhom.eu/scripts/site_gates.py` is run, R-29's three orphans are named nowhere and have stopped nothing. Controller-side enforcement was rejected because a load-time check reads only the file and a static audit reports the catalog clean **including papra** — it would pass on the very defect it exists to catch; CI was rejected for now (neither repo has any, no users yet). Adding a fourth gate here means adding it to `GATES` in this file — nothing else. | diff --git a/scripts/catalog_gates.py b/scripts/catalog_gates.py index 28c0fcc..da3fd78 100644 --- a/scripts/catalog_gates.py +++ b/scripts/catalog_gates.py @@ -97,6 +97,9 @@ GATES = [ # defect it catches does not merely mis-colour a badge, it makes a SUCCESSFUL update stop a # working app (the `verifying` phase waits on this probe), so it must bite at push time. ("probe-matches-compose", "check-probe-matches-compose.py", True, True, False), + # R-715 (2026-09-29): a setup gate's `setup_done_probe:` must carry its measured before/after answers directly + # above it. A guessed probe that never flips keeps an app closed to all but the household AND blocks their press. + ("probe-measured", "check-probe-measured.py", True, True, False), # 2026-09-23 (`09` §3 decision 13, §6.4 part 4): THE TEST RECORD. The static half needs no # history and no network, so it bites in CI too: a ladder must be well-formed and its newest step # must BE the compose's images. The move half needs history (skipped out loud on CI's shallow diff --git a/scripts/check-probe-measured.py b/scripts/check-probe-measured.py new file mode 100644 index 0000000..de64a55 --- /dev/null +++ b/scripts/check-probe-measured.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""check-probe-measured.py — a `setup_done_probe:` must say what it MEASURED, before and after (R-715). + +WHY. The setup gate (`09` §3 decision 46) opens an app when its own status says the first admin exists. A probe +that never flips keeps the app closed to everyone but the household — and it also makes the household's "Done" +press refuse (the press asks the probe first). Measured 2026-09-29 on gramps-web: its status answered 405 after the +setup, the box read only 200s, and the press was refused until the catalog was fixed. And zipline's upstream status +route answered 403 after the setup. A probe copied from upstream docs is a guess; only a before/after measurement on +a real install is evidence. + +THE RULE. The comment block directly above `setup_done_probe:` (the contiguous `#` lines) must carry: + * the word "measured", + * a date (YYYY-MM-DD), and + * both answers: "before … after …", or " -> ". +A comment elsewhere in the file (the tagline, another block) does not count. + +Run from the repo root: python3 scripts/check-probe-measured.py [--root=DIR] [app ...] +Exit 0 all probes carry a measurement · 1 a probe without one. +""" +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DATE = re.compile(r"\b20\d\d-\d\d-\d\d\b") + + +def comment_above(lines, i): + out = [] + j = i - 1 + while j >= 0 and lines[j].lstrip().startswith("#"): + out.append(lines[j].lstrip()[1:].strip()) + j -= 1 + return " ".join(reversed(out)) + + +def check(path): + lines = open(path, encoding="utf-8").read().split("\n") + for i, l in enumerate(lines): + if l.startswith("setup_done_probe:"): + c = comment_above(lines, i) + low = c.lower() + has_pair = "->" in c or ("before" in low and "after" in low) + if "measured" not in low or not DATE.search(c) or not has_pair: + return "the comment above setup_done_probe: lacks %s (found: %r)" % ( + ", ".join(x for x, ok in (("the word 'measured'", "measured" in low), ("a date", bool(DATE.search(c))), + ("the before/after answers", has_pair)) if not ok), c[:160]) + return None + + +def main(): + root = ROOT + apps = [] + for a in sys.argv[1:]: + if a.startswith("--root="): + root = a.split("=", 1)[1] + else: + apps.append(a) + tdir = os.path.join(root, "templates") + names = apps or sorted(d for d in os.listdir(tdir) if os.path.isdir(os.path.join(tdir, d))) + bad, n = [], 0 + for app in names: + for dp, _, files in os.walk(os.path.join(tdir, app)): + if ".felhom.yml" in files and os.path.basename(dp) == app: + p = os.path.join(dp, ".felhom.yml") + if "\nsetup_done_probe:" in open(p, encoding="utf-8").read(): + n += 1 + why = check(p) + if why: + bad.append("%s: %s" % (app, why)) + if bad: + print("probe-measured: FAIL — %d of %d probes carry no measurement:" % (len(bad), n)) + for b in bad: + print(" - " + b) + return 1 + print("probe-measured: OK — %d probes, each with a measured before/after" % n) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/copy_freeze/hu.json b/scripts/copy_freeze/hu.json index f25049e..1bd0156 100644 --- a/scripts/copy_freeze/hu.json +++ b/scripts/copy_freeze/hu.json @@ -1078,8 +1078,9 @@ "description": "Feladatkezelő listák és táblák (Todoist/Trello alternatíva)" }, "wanderer": { + "app_info.add_people": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál.", "app_info.first_steps[0]": "Nyisd meg a hike.DOMAIN címet a böngészőben", - "app_info.first_steps[1]": "Hozd létre a fiókodat azonnal a telepítés után. Figyelem: ebben az alkalmazásban bárki, aki megtalálja a címet, fiókot hozhat létre.", + "app_info.first_steps[1]": "Hozd létre a fiókodat azonnal a telepítés után, aztán az alkalmazás oldalán zárd le a regisztrációt. Addig bárki, aki megtalálja a címet, fiókot hozhat létre.", "app_info.first_steps[2]": "Importálj egy GPX fájlt vagy tervezz új útvonalat", "app_info.first_steps[3]": "Fedezd fel a térképes megjelenítést", "app_info.tagline": "Túra tervező - útvonalak, GPX nyomok és domborzati térképek", diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index 143add1..82a9695 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -54,6 +54,7 @@ COVERS = { "the app listens; vs a real probe port/path that the app does not answer (R-618)", "test-record": "a ladder whose newest step is not the compose's images (a move without a record), a gap, a line that is not one JSON entry, a failed verdict - vs a clean ladder (09 decision 13)", "test-record-move": "an image move with NO entry, with the entry only in a COMMENT or in README, with a failed/backfilled entry, with a digest the registry no longer serves, memory_tight without a raised limit - vs a proven entry that matches; a ref moving in a compose COMMENT is not a move (09 decision 13)", + "probe-measured": "the measurement written in the TAGLINE or another comment block, not directly above setup_done_probe:; a date with no before/after; before/after with no date; 'read upstream' instead of 'measured' - vs a genuine measured comment (R-715)", "copy-i18n": "Hungarian edited in a COMMENT/README/display_name (label, not copy) vs a real frozen string changed; an English block that is not English, is not matched to a Hungarian twin, or rewrites a credential (R-560). Also the DEGRADED mode CI actually runs — PyYAML shadowed out, freeze only (R-595)", } @@ -469,19 +470,24 @@ def main(): # 11.6 -> 11.8 through its own test record, and a literal here broke every case below # ("fixture drifted") without a single gate changing. R-663. KDB = cur_image(clone, KIMAI, "kimai-db") + # docmost-postgres too (2026-09-29): it moved 16 -> 18 through its own ladder, and the typed "16-alpine" here + # made every run fail "fixture drifted" before a single case ran — the same shape as R-663. Read it. + DMDB = cur_image(clone, DOCMOST, "docmost-postgres") + DM_MAJ = int(DMDB.split(":")[1].split("-")[0].split(".")[0]) + DM_NEXT = DMDB.replace(":%d" % DM_MAJ, ":%d" % (DM_MAJ + 1), 1) if not KDB.startswith("mariadb:11."): raise SystemExit("kimai-db is %s — the cases below assume a MariaDB 11 line; fixture drifted" % KDB) # ── THE FACTS: these must be refused ───────────────────────────────────────────────── - out = case("FACT: docmost-postgres 16-alpine -> 17-alpine bundled with the app bump", clone, - [(DOCMOST, lambda t: swap_image("docmost-postgres", "postgres:16-alpine", "postgres:17-alpine")(t))], + out = case("FACT: docmost-postgres major + 1 (read from the clone)", clone, + [(DOCMOST, lambda t: swap_image("docmost-postgres", DMDB, DM_NEXT)(t))], expect_rc=1, - must_contain=("ENGINE-MAJOR GATE FAILED", "docmost-postgres", "postgres 16 -> 17")) + must_contain=("ENGINE-MAJOR GATE FAILED", "docmost-postgres", "postgres %d -> %d" % (DM_MAJ, DM_MAJ + 1))) if "REFUSAL_TEXT" in os.environ: print(out) - case("FACT: docmost-postgres postgres:16-alpine -> 17-alpine", clone, - [(DOCMOST, swap_image("docmost-postgres", "postgres:16-alpine", "postgres:17-alpine"))], - expect_rc=1, must_contain=("docmost-postgres", "postgres 16 -> 17", "R-463")) + case("FACT: docmost-postgres major + 1, alone", clone, + [(DOCMOST, swap_image("docmost-postgres", DMDB, DM_NEXT))], + expect_rc=1, must_contain=("docmost-postgres", "postgres %d -> %d" % (DM_MAJ, DM_MAJ + 1), "R-463")) # R-469 + R-450 (2026-09-21): a MariaDB major bundled with the app's own bump is the # bookstack 0b73e5e shape — two migrations behind one edge — and stays refused. case("FACT: kimai-db 11.6 -> 12.3 BUNDLED with the kimai app bump", clone, @@ -508,22 +514,24 @@ def main(): DOCMOST_FY = "templates/docmost/.felhom.yml" DM, DR = cur_image(clone, DOCMOST, "docmost"), cur_image(clone, DOCMOST, "docmost-redis") DPG = cur_image(clone, DOCMOST, "docmost-postgres") - if not DPG.startswith("postgres:16"): - raise SystemExit("docmost-postgres is %s — the cases below assume 16; fixture drifted" % DPG) - def pg_entry(mark=True, box=True, to_pg="postgres:18-alpine", extra=""): + # Read, never typed (2026-09-29: docmost moved 16 -> 18 and the typed 16 stopped every run). The conversion + # case moves the CURRENT major one up. + PG_FROM = int(DPG.split(":")[1].split("-")[0].split(".")[0]); PG_TO = PG_FROM + 1 + PG_TO_REF = DPG.replace(":%d" % PG_FROM, ":%d" % PG_TO, 1) + def pg_entry(mark=True, box=True, to_pg=None, extra=""): e = {"from": {"docmost": DM, "docmost-postgres": DPG, "docmost-redis": DR}, - "to": {"docmost": DM, "docmost-postgres": to_pg, "docmost-redis": DR}, + "to": {"docmost": DM, "docmost-postgres": to_pg or PG_TO_REF, "docmost-redis": DR}, "digest": {"docmost": "sha256:" + "a" * 64, "docmost-postgres": "sha256:" + "b" * 64, "docmost-redis": "sha256:" + "c" * 64}, "verdict": "proven", "tested_at": "2026-09-25T20:00:00Z", "harness_version": 4, "evidence": "x/bench.json", "box_evidence": "x/box.json" if box else None, "memory_peak_pct": 20.0, "marks": {"files_may_change": False, "needs_person": None, "memory_tight": False}} if mark: - e["engine_conversion"] = {"service": "docmost-postgres", "engine": "postgres", "from": 16, "to": 18} + e["engine_conversion"] = {"service": "docmost-postgres", "engine": "postgres", "from": PG_FROM, "to": PG_TO} return lambda t: t.rstrip("\n") + "\n - " + json.dumps(e) + "\n" + extra - pg18 = swap_image("docmost-postgres", DPG, "postgres:18-alpine") - case("GENUINE: docmost-postgres 16 -> 18 ALONE with its proven two-venue MARKED entry", clone, + pg18 = swap_image("docmost-postgres", DPG, PG_TO_REF) + case("GENUINE: docmost-postgres major+1 ALONE with its proven two-venue MARKED entry", clone, [(DOCMOST, pg18), (DOCMOST_FY, pg_entry())], - expect_rc=0, must_contain=("ALLOWED", "docmost-postgres", "postgres 16 -> 18", "decision 35")) + expect_rc=0, must_contain=("ALLOWED", "docmost-postgres", "postgres %d -> %d" % (PG_FROM, PG_TO), "decision 35")) case("DECOY: the entry is proven on both venues but carries NO conversion mark", clone, [(DOCMOST, pg18), (DOCMOST_FY, pg_entry(mark=False))], expect_rc=1, must_contain=("NOT PROVEN FOR THIS APP", "engine_conversion None")) @@ -531,7 +539,7 @@ def main(): [(DOCMOST, pg18), (DOCMOST_FY, pg_entry(box=False))], expect_rc=1, must_contain=("NOT PROVEN FOR THIS APP", "BOTH venues")) case("DECOY: the mark sits in a COMMENT, the entry has none", clone, - [(DOCMOST, pg18), (DOCMOST_FY, pg_entry(mark=False, extra='# engine_conversion: {"service": "docmost-postgres", "engine": "postgres", "from": 16, "to": 18}\n'))], + [(DOCMOST, pg18), (DOCMOST_FY, pg_entry(mark=False, extra='# engine_conversion: {"service": "docmost-postgres", "engine": "postgres", "from": PG_FROM, "to": PG_TO}\n'))], expect_rc=1, must_contain=("NOT PROVEN FOR THIS APP",)) case("DECOY: the marked entry, but the move is BUNDLED with the app's own bump", clone, [(DOCMOST, lambda t: swap_image("docmost", DM, DM + "-next")(pg18(t))), (DOCMOST_FY, pg_entry())], @@ -882,6 +890,34 @@ i18n: test_record_cases(clone) + # probe-measured (R-715): the measurement must sit DIRECTLY above setup_done_probe:, with a date and both answers. + KOMGA = "templates/komga/.felhom.yml" + def drop_note(t): + return "\n".join(l for l in t.split("\n") if not (l.startswith("# measured on 9202") and "isClaimed" in l)) + def case_pm(name, edits, expect_rc, must=()): + global ran + ran += 1 + try: + for relpath, fn in edits: + edit(clone, relpath, fn) + r = sh([sys.executable, os.path.join(ROOT, "scripts", "check-probe-measured.py"), "--root=" + clone, "komga"], cwd=clone) + out = r.stdout + r.stderr + if r.returncode == expect_rc and all(m in out for m in must): + print(" ok %-52s rc=%d (expected %d)" % (name, r.returncode, expect_rc)) + else: + fails.append("%s: rc=%d expected %d\n%s" % (name, r.returncode, expect_rc, out[-600:])) + finally: + sh(["git", "checkout", "-q", "--", "."], cwd=clone) + case_pm("probe-measured: genuine komga note passes", [], 0, ("probe-measured: OK",)) + case_pm("probe-measured: note moved into the TAGLINE", [(KOMGA, lambda t: drop_note(t).replace( + "\ntagline:", "\ntagline:", 1).replace("app_info:\n", "app_info:\n # measured on 9202 2026-09-29: isClaimed false -> true\n", 1))], 1, ("FAIL",)) + case_pm("probe-measured: date but no before/after", [(KOMGA, lambda t: drop_note(t).replace( + "\nsetup_done_probe:", "\n# measured on 9202 2026-09-29\nsetup_done_probe:"))], 1, ("before/after",)) + case_pm("probe-measured: before/after but no date", [(KOMGA, lambda t: drop_note(t).replace( + "\nsetup_done_probe:", "\n# measured: isClaimed false -> true\nsetup_done_probe:"))], 1, ("a date",)) + case_pm("probe-measured: 'read upstream' is not a measurement", [(KOMGA, lambda t: drop_note(t).replace( + "\nsetup_done_probe:", "\n# read upstream 2026-09-29: isClaimed false -> true\nsetup_done_probe:"))], 1, ("measured",)) + finally: shutil.rmtree(clone, ignore_errors=True) diff --git a/templates/adventurelog/.felhom.yml b/templates/adventurelog/.felhom.yml index bfd3527..5a164b8 100644 --- a/templates/adventurelog/.felhom.yml +++ b/templates/adventurelog/.felhom.yml @@ -55,8 +55,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "Path(`/signup`) || PathPrefix(`/auth/browser/v1/auth/signup`) || PathPrefix(`/_allauth/browser/v1/auth/signup`) || PathPrefix(`/accounts/signup`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+(signup/*$|auth/+browser/+v1/+auth/+signup|_allauth/+browser/+v1/+auth/+signup|accounts/+signup)`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; it also refuses the household's own first account, so it goes on AFTER the setup). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_CLOSED: "true" # --- App info (info page content) --- app_info: diff --git a/templates/adventurelog/docker-compose.yml b/templates/adventurelog/docker-compose.yml index a77091b..62991ca 100644 --- a/templates/adventurelog/docker-compose.yml +++ b/templates/adventurelog/docker-compose.yml @@ -17,6 +17,8 @@ services: adventurelog-postgres: condition: service_healthy environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - DISABLE_REGISTRATION=${SIGNUP_CLOSED:-false} - DJANGO_SECRET_KEY=${SECRET_KEY} - SECRET_KEY=${SECRET_KEY} # R-482: the image defaults DEBUG to True (settings: getenv('DEBUG','True')), which serves full diff --git a/templates/audiobookshelf/.felhom.yml b/templates/audiobookshelf/.felhom.yml index 7641f05..4fa4ea4 100644 --- a/templates/audiobookshelf/.felhom.yml +++ b/templates/audiobookshelf/.felhom.yml @@ -59,6 +59,7 @@ deploy_fields: # signed in to the dashboard) until the first setup is done; audiobookshelf's own status says so (`/status` isInit — upstream, measured on 9202 by the # 2026-09-29 live proof). setup_gate: true +# measured on 9202 2026-09-29: isInit false -> true after POST /init; the gate opened ~20 s later (audits/login-gate-2026-09-29/C). setup_done_probe: url: http://audiobookshelf:80/status field: isInit diff --git a/templates/calcom/.felhom.yml b/templates/calcom/.felhom.yml index ad7edc9..1f41caa 100644 --- a/templates/calcom/.felhom.yml +++ b/templates/calcom/.felhom.yml @@ -56,8 +56,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's POST /api/auth/signup created an account (201) after the setup). -signup_block: "Path(`/signup`) || PathPrefix(`/auth/signup`) || PathPrefix(`/api/auth/signup`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's POST /api/auth/signup created an account (201) after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+(signup|auth/+signup|api/+auth/+signup)(/|$)`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; safe from install too (the first admin is /auth/setup)). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_CLOSED: "true" # --- App info (info page content) --- app_info: diff --git a/templates/calcom/docker-compose.yml b/templates/calcom/docker-compose.yml index 800fa2d..6a8f4e5 100644 --- a/templates/calcom/docker-compose.yml +++ b/templates/calcom/docker-compose.yml @@ -18,6 +18,8 @@ services: calcom-postgres: condition: service_healthy environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - NEXT_PUBLIC_DISABLE_SIGNUP=${SIGNUP_CLOSED:-false} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} - DATABASE_URL=postgresql://calcom:${DB_PASSWORD}@calcom-postgres:5432/calcom diff --git a/templates/ghost/.felhom.yml b/templates/ghost/.felhom.yml index a9d57b0..fbd12ec 100644 --- a/templates/ghost/.felhom.yml +++ b/templates/ghost/.felhom.yml @@ -40,6 +40,11 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true +# measured on 9202 2026-09-29: before the setup {"setup":[{"status":false}]}, after {"setup":[{"status":true}]}. +setup_done_probe: + url: http://ghost:2368/ghost/api/admin/authentication/setup/ + field: setup.0.status + done: "true" # --- App info (info page content) --- app_info: diff --git a/templates/gitea/.felhom.yml b/templates/gitea/.felhom.yml index e8b801b..3dafe1a 100644 --- a/templates/gitea/.felhom.yml +++ b/templates/gitea/.felhom.yml @@ -38,8 +38,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: /user/sign_up served the registration form after the setup). -signup_block: "PathPrefix(`/user/sign_up`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: /user/sign_up served the registration form after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+user/+sign_up`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; safe from install too (the installer makes the admin)). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_CLOSED: "true" # --- App info (info page content) --- app_info: diff --git a/templates/gitea/docker-compose.yml b/templates/gitea/docker-compose.yml index 6224da1..890087c 100644 --- a/templates/gitea/docker-compose.yml +++ b/templates/gitea/docker-compose.yml @@ -12,6 +12,8 @@ services: container_name: gitea restart: unless-stopped environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - GITEA__service__DISABLE_REGISTRATION=${SIGNUP_CLOSED:-false} - TZ=Europe/Budapest - GITEA__server__ROOT_URL=https://${SUBDOMAIN}.${DOMAIN} - GITEA__server__SSH_DOMAIN=${SUBDOMAIN}.${DOMAIN} diff --git a/templates/gramps-web/.felhom.yml b/templates/gramps-web/.felhom.yml index 285ec5b..cc10a8d 100644 --- a/templates/gramps-web/.felhom.yml +++ b/templates/gramps-web/.felhom.yml @@ -44,8 +44,20 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: the self-registration address answered (500 here); closed to be safe). -signup_block: "PathRegexp(`^/api/users/[^/]+/register/`)" +# measured on 9202 2026-09-29: before the setup HTTP 200 with an owner token, after HTTP 405 "Users already exist". +setup_done_probe: + url: http://gramps-web:5000/api/token/create_owner/ + field: error.code + done: "405" + done_status: 405 +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: the self-registration address answered (500 here); closed to be safe; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+api/+users/+[^/]+/+register`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; safe from install too (create_owner)). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_CLOSED: "true" # --- App info (info page content) --- app_info: diff --git a/templates/gramps-web/docker-compose.yml b/templates/gramps-web/docker-compose.yml index 5fcce6a..a69bbb6 100644 --- a/templates/gramps-web/docker-compose.yml +++ b/templates/gramps-web/docker-compose.yml @@ -13,6 +13,8 @@ services: container_name: gramps-web restart: unless-stopped environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - GRAMPSWEB_REGISTRATION_DISABLED=${SIGNUP_CLOSED:-false} - TZ=Europe/Budapest - GRAMPSWEB_TREE=Family Tree - GRAMPSWEB_SECRET_KEY=${GRAMPSWEB_SECRET_KEY} diff --git a/templates/home-assistant/.felhom.yml b/templates/home-assistant/.felhom.yml index ff97fd5..fb5a189 100644 --- a/templates/home-assistant/.felhom.yml +++ b/templates/home-assistant/.felhom.yml @@ -38,6 +38,11 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true +# measured on 9202 2026-09-29: before [{"step":"user","done":false},…], after the user step [{"step":"user","done":true},…]. +setup_done_probe: + url: http://home-assistant:8123/api/onboarding + field: 0.done + done: "true" # --- App info (info page content) --- app_info: diff --git a/templates/homebox/.felhom.yml b/templates/homebox/.felhom.yml index f8e1907..3343790 100644 --- a/templates/homebox/.felhom.yml +++ b/templates/homebox/.felhom.yml @@ -53,8 +53,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "PathPrefix(`/api/v1/users/register`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+api/+v1/+users/+register`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; it also refuses the household's own first account, so it goes on AFTER the setup). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_OPEN: "false" # --- App info (info page content) --- app_info: diff --git a/templates/homebox/docker-compose.yml b/templates/homebox/docker-compose.yml index 3b27455..849300d 100644 --- a/templates/homebox/docker-compose.yml +++ b/templates/homebox/docker-compose.yml @@ -15,6 +15,8 @@ services: container_name: homebox restart: unless-stopped environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - HBOX_OPTIONS_ALLOW_REGISTRATION=${SIGNUP_OPEN:-true} - HBOX_LOG_LEVEL=info - HBOX_LOG_FORMAT=text - HBOX_WEB_MAX_UPLOAD_SIZE=50 diff --git a/templates/immich/.felhom.yml b/templates/immich/.felhom.yml index abce739..6203d7e 100644 --- a/templates/immich/.felhom.yml +++ b/templates/immich/.felhom.yml @@ -69,6 +69,7 @@ deploy_fields: # signed in to the dashboard) until the first setup is done; immich's own status says so (measured on 9202 2026-09-29: # isInitialized false -> true once the admin exists). setup_gate: true +# measured on 9202 2026-09-29: isInitialized false -> true after the admin sign-up (audits/login-gate-2026-09-29/C). setup_done_probe: url: http://immich-server:2283/api/server/config field: isInitialized diff --git a/templates/n8n/.felhom.yml b/templates/n8n/.felhom.yml index c333ced..c842c9a 100644 --- a/templates/n8n/.felhom.yml +++ b/templates/n8n/.felhom.yml @@ -47,6 +47,7 @@ deploy_fields: # signed in to the dashboard) until the first setup is done; n8n's own settings say so (measured on 9202 2026-09-29: # showSetupOnFirstLoad true -> false once the owner exists). setup_gate: true +# measured on 9202 2026-09-29: showSetupOnFirstLoad true -> false after the owner setup (audits/login-gate-2026-09-29/C). setup_done_probe: url: http://n8n:5678/rest/settings field: data.userManagement.showSetupOnFirstLoad diff --git a/templates/opengist/.felhom.yml b/templates/opengist/.felhom.yml index 3fcd034..6da9297 100644 --- a/templates/opengist/.felhom.yml +++ b/templates/opengist/.felhom.yml @@ -38,8 +38,8 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "PathPrefix(`/-/register`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+-/+register`)" # --- App info (info page content) --- app_info: diff --git a/templates/papra/.felhom.yml b/templates/papra/.felhom.yml index a828d02..15c568e 100644 --- a/templates/papra/.felhom.yml +++ b/templates/papra/.felhom.yml @@ -49,8 +49,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "PathPrefix(`/api/auth/sign-up`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+api/+auth/+sign-up`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; it also refuses the household's own first account, so it goes on AFTER the setup). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_OPEN: "false" # --- App info (info page content) --- app_info: diff --git a/templates/papra/docker-compose.yml b/templates/papra/docker-compose.yml index 28ba6eb..18f68ee 100644 --- a/templates/papra/docker-compose.yml +++ b/templates/papra/docker-compose.yml @@ -12,6 +12,8 @@ services: container_name: papra restart: unless-stopped environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - AUTH_IS_REGISTRATION_ENABLED=${SIGNUP_OPEN:-true} - TZ=Europe/Budapest - APP_BASE_URL=https://${SUBDOMAIN}.${DOMAIN} - AUTH_SECRET=${AUTH_SECRET} diff --git a/templates/sparkyfitness/.felhom.yml b/templates/sparkyfitness/.felhom.yml index 95fccb9..3454edb 100644 --- a/templates/sparkyfitness/.felhom.yml +++ b/templates/sparkyfitness/.felhom.yml @@ -67,8 +67,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "PathPrefix(`/api/auth/sign-up`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+api/+auth/+sign-up`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; it also refuses the household's own first account, so it goes on AFTER the setup). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_CLOSED: "true" app_info: tagline: "Táplálkozási napló, kalóriaszámláló és edzéskövető – önállóan üzemeltetve" diff --git a/templates/sparkyfitness/docker-compose.yml b/templates/sparkyfitness/docker-compose.yml index e9fc510..efc5f2f 100644 --- a/templates/sparkyfitness/docker-compose.yml +++ b/templates/sparkyfitness/docker-compose.yml @@ -49,6 +49,8 @@ services: sparkyfitness-db: condition: service_healthy environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - SPARKY_FITNESS_DISABLE_SIGNUP=${SIGNUP_CLOSED:-false} - SPARKY_FITNESS_DB_HOST=sparkyfitness-db - SPARKY_FITNESS_DB_PORT=5432 - SPARKY_FITNESS_DB_NAME=sparkyfitness_db @@ -59,7 +61,6 @@ services: - SPARKY_FITNESS_API_ENCRYPTION_KEY=${API_ENCRYPTION_KEY} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - SPARKY_FITNESS_FRONTEND_URL=https://${SUBDOMAIN}.${DOMAIN} - - SPARKY_FITNESS_DISABLE_SIGNUP=false - ALLOW_PRIVATE_NETWORK_CORS=true - SPARKY_FITNESS_LOG_LEVEL=INFO - NODE_ENV=production diff --git a/templates/termix/.felhom.yml b/templates/termix/.felhom.yml index 8fd3e84..6277f6a 100644 --- a/templates/termix/.felhom.yml +++ b/templates/termix/.felhom.yml @@ -43,8 +43,14 @@ setup_done_probe: url: http://termix:8080/users/setup-required field: setup_required done: "false" -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "PathPrefix(`/users/create`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+users/+create`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; it also refuses the household's own first account, so it goes on AFTER the setup). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_OPEN: "false" # --- App info (info page content) --- app_info: diff --git a/templates/termix/docker-compose.yml b/templates/termix/docker-compose.yml index 89a3ee9..8cd60a2 100644 --- a/templates/termix/docker-compose.yml +++ b/templates/termix/docker-compose.yml @@ -12,6 +12,8 @@ services: container_name: termix restart: unless-stopped environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - ALLOW_REGISTRATION=${SIGNUP_OPEN:-true} - TZ=Europe/Budapest - PORT=8080 volumes: diff --git a/templates/vikunja/.felhom.yml b/templates/vikunja/.felhom.yml index b805ed5..0fc8173 100644 --- a/templates/vikunja/.felhom.yml +++ b/templates/vikunja/.felhom.yml @@ -44,8 +44,14 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "PathPrefix(`/api/v1/register`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+api/+v1/+register`)" +# The app's OWN sign-up switch, set once the gate opens (measured on 9202 2026-09-29: with it on a stranger's +# sign-up is refused even straight at the app; it also refuses the household's own first account, so it goes on AFTER the setup). The compose default stays open, so an installed app is +# unchanged by the catalog. +after_setup: + env: + SIGNUP_OPEN: "false" # --- App info (info page content) --- app_info: diff --git a/templates/vikunja/docker-compose.yml b/templates/vikunja/docker-compose.yml index 85073b3..cca9691 100644 --- a/templates/vikunja/docker-compose.yml +++ b/templates/vikunja/docker-compose.yml @@ -14,6 +14,8 @@ services: user: "0:0" restart: unless-stopped environment: + # decision 47: the app's own sign-up switch — open until the box closes it after the first admin (after_setup) + - VIKUNJA_SERVICE_ENABLEREGISTRATION=${SIGNUP_OPEN:-true} - TZ=Europe/Budapest - VIKUNJA_SERVICE_PUBLICURL=https://${SUBDOMAIN}.${DOMAIN} - VIKUNJA_SERVICE_JWTSECRET=${VIKUNJA_SERVICE_JWTSECRET} diff --git a/templates/wanderer/.felhom.yml b/templates/wanderer/.felhom.yml index e043e4f..b5d0bfb 100644 --- a/templates/wanderer/.felhom.yml +++ b/templates/wanderer/.felhom.yml @@ -55,9 +55,22 @@ deploy_fields: generate: "hex:16" locked_after_deploy: true +# --- Sign-up (controller >= 0.282.0, `09` §3 decisions 47-48) --- +# NOT gated: its web server calls its own database through the public name, which a gate would refuse (measured on +# 9202 2026-09-29, R-714). No first-admin screen to take: PocketBase's superuser installer needs the one-time link +# from the server log. The door is SIGN-UP — in the web and straight through PocketBase's API (measured 2026-09-29: +# a stranger's POST /api/collections/users/records answered 200 — and PocketBase takes the collection by its id +# `_pb_users_auth_` and in ANY letter case, hence the case-insensitive pattern). After the household makes its account, the app +# page offers "Close sign-up now": both addresses answer "sign-up is closed", and wanderer's own switch goes on. +signup_block: "Path(`/register`) || (PathRegexp(`(?i)^/+api/+collections/+(users|_pb_users_auth_)/+records`) && Method(`POST`))" +after_setup: + env: + SIGNUP_CLOSED: "true" + # --- App info (info page content) --- app_info: tagline: "Túra tervező - útvonalak, GPX nyomok és domborzati térképek" + add_people: "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál." docs_url: "https://wanderer.to/" use_cases: @@ -69,7 +82,7 @@ app_info: first_steps: - 'Nyisd meg a hike.DOMAIN címet a böngészőben' - - 'Hozd létre a fiókodat azonnal a telepítés után. Figyelem: ebben az alkalmazásban bárki, aki megtalálja a címet, fiókot hozhat létre.' + - 'Hozd létre a fiókodat azonnal a telepítés után, aztán az alkalmazás oldalán zárd le a regisztrációt. Addig bárki, aki megtalálja a címet, fiókot hozhat létre.' - 'Importálj egy GPX fájlt vagy tervezz új útvonalat' - 'Fedezd fel a térképes megjelenítést' @@ -89,6 +102,7 @@ i18n: description: 'A hike planner that follows your tracks' app_info: tagline: 'A hike planner - routes, GPX tracks and terrain maps' + add_people: 'Open sign-up for 15 minutes, and your family member signs up.' use_cases: - 'Plan and keep hiking routes' - 'Import and export GPX files' @@ -97,7 +111,7 @@ i18n: - 'Share a route with other people' first_steps: - 'Open hike.DOMAIN in your browser' - - 'Create your account right after the install. Note: in this app, anyone who finds the address can make an account.' + - 'Create your account right after the install, then close sign-up on the app page. Until then, anyone who finds the address can make an account.' - 'Import a GPX file, or plan a new route' - 'Have a look at the map view' deploy_fields: diff --git a/templates/wanderer/docker-compose.yml b/templates/wanderer/docker-compose.yml index c1a360c..cad5450 100644 --- a/templates/wanderer/docker-compose.yml +++ b/templates/wanderer/docker-compose.yml @@ -38,7 +38,8 @@ services: - ORIGIN=https://${SUBDOMAIN}.${DOMAIN} - PUBLIC_POCKETBASE_URL=https://${SUBDOMAIN_DB}.${DOMAIN} - BODY_SIZE_LIMIT=Infinity - - PUBLIC_DISABLE_SIGNUP=false + # decision 47/48: open until the household closes sign-up (after_setup / close sign-up now) + - PUBLIC_DISABLE_SIGNUP=${SIGNUP_CLOSED:-false} - UPLOAD_FOLDER=/app/uploads - PUBLIC_MAP_MAX_POLYLINES=100 - OVERPASS_API_URL=https://overpass-api.de diff --git a/templates/wishlist/.felhom.yml b/templates/wishlist/.felhom.yml index c496a14..be2ed33 100644 --- a/templates/wishlist/.felhom.yml +++ b/templates/wishlist/.felhom.yml @@ -38,8 +38,8 @@ deploy_fields: # --- The setup gate (controller >= 0.281.0, `09` §3 decisions 46-47) --- # The first visitor would create the admin; a fresh install is closed to everyone but the household until the first setup is done. setup_gate: true -# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup). -signup_block: "Path(`/signup`)" +# Decision 47: the app's own sign-up address is closed once the gate opens (measured on 9202 2026-09-29: a stranger's sign-up succeeded after the setup; case-insensitive and slash-tolerant because termix's router ignores case (measured 2026-09-29)). +signup_block: "PathRegexp(`(?i)^/+signup/*$`)" # --- App info (info page content) --- app_info: