From 447eeaeba45735ca96c4a4b4dd97df9986f7456a Mon Sep 17 00:00:00 2001 From: matcha Date: Sun, 4 Oct 2026 02:57:00 +0000 Subject: [PATCH] Finish Persims consolidation --- docs/persimmons-gitea-reset.md | 144 +++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/persimmons-gitea-reset.md diff --git a/docs/persimmons-gitea-reset.md b/docs/persimmons-gitea-reset.md new file mode 100644 index 0000000..b8a5da3 --- /dev/null +++ b/docs/persimmons-gitea-reset.md @@ -0,0 +1,144 @@ +# Dedicated Persimmons identity and vaults + +Persimmons keeps its existing product host and extension crypto. Signup provisions +a non-admin user in **git.persimmons.app**, a dedicated Gitea on the Fly app +**persimmons-gitea** with a single 1 GiB **persimmons_gitea_data** volume in iad. +It does not use Teahub Gitea for product identities or repositories. + +The independent `persimmons-launch.yml` Actions operator holds FLY_INFRA and +CLOUD_DNS. It deploys the dedicated Gitea, establishes DNS/HTTPS, checks +`/api/healthz` including database/cache, and runs a fixed bootstrap program via +Fly Machines exec. The bootstrap creates the provisioning admin before any product +signup, with a random unrecoverable login password and an API token scoped to +`write:admin,read:user,read:repository`. A root-only 0700 directory/0600 file on +the dedicated Gitea volume makes bootstrap resumable. The operator captures that +credential response and stages it with `fly secrets import` stdin exclusively on +**persimmons-app** as PERSIMMONS_GITEA_TOKEN. No provider credential is sent to +product or Gitea runtime; no token enters argv, receipts, logs, source, or clients. +Public Gitea registration and Actions are disabled; repositories are private. + +The application has no generic admin proxy. It uses only fixed user provisioning, +identity lookup and create-repository endpoints. Password login uses Basic auth +over pinned HTTPS to Gitea `/api/v1/user` with redirects refused. Passwords and +Gitea user tokens are not retained. Opaque 12-hour HttpOnly/Secure sessions contain +only a digest, immutable Gitea ID, login, owner `pg-`, and expiry in SQLite. +Admin identities cannot sign in to the product. Username/email conflict is 409. +Generated email aliases are private placeholders; no email delivery/recovery is +implemented. Gitea account renames require a fresh product login for named vaults. +Existing product sessions otherwise remain valid until logout or their expiry. + +The first database open upgrades the disposable local schema to version 2. Under +one transaction it drops only old users/sessions/throttle tables and the local +`/data/human-vault.sqlite` human_items table. It never resets a version-2 store, +and rejects unknown future schema versions. This is the explicitly authorized +fresh-account reset. Existing browser keys use a different owner namespace and +are not imported. Client-side wrapped keys and encrypted credential capture/fill +remain unchanged, as do accepted server-visible origin/username metadata. + +`GET /api/vaults` lists only recorded creations belonging to the authenticated +user. `POST /api/vaults` accepts exactly `{name}` from the product origin with the +Persimmons request header; caller-supplied owner, repo ID, or privacy is rejected. +Whitespace becomes hyphens; valid names otherwise retain spelling. The service +checks the live Gitea user ID before creating on behalf of that user, requests +`private=true,auto_init=false`, and verifies both the creation response and a fresh +GET for the same immutable repo ID, owner ID/login, privacy, and emptiness before +recording it. Duplicate/existing repositories are conflicts, never imported. +Lists recheck identity, repo ID, ownership and privacy; transferred, replaced or +public repositories fail closed. An interrupted create can leave an empty private +repo without a local record: retries conflict instead of silently adopting it. + +Named vaults appear below Master vault. Empty repositories are valid. Juice, +Actions/scoping, and credential rotation remain future work; retained legacy code +and documentation are preserved. New named repositories can later host scoped +credentials without changing Master-vault ownership or encryption. + +Validation: `python persimmons/build.py`, standalone host/Gitea/UX tests, extension +Node tests, operator HTTP acceptance, then `tests/persimmons-host-browser.cjs` +against live UI/API. The live browser harness creates a fresh non-admin user, +creates `work-test` through the modal, verifies it independently on Gitea, rejects +a duplicate, links/initializes Master vault in the real extension, captures/fills +without submitting, deletes the synthetic ciphertext, and verifies logout. +Synthetic test identities and empty private repositories remain as evidence; +passwords, sessions, and plaintext never appear in receipts. + +Provider references: [Gitea CLI bootstrap and scoped tokens](https://docs.gitea.com/administration/command-line/), +[Gitea create user API](https://docs.gitea.com/api/1.23/operations/admin-create-user/), +[Gitea create repository on behalf of a user](https://docs.gitea.com/api/1.23/operations/admin-create-repo/), +and [Fly Machines API](https://docs.fly.io/machines/api/machines-resource/). + +## Accepted live deployment — 2026-10-01 + +**Done.** Deployed source **`2f8fcc8208205b30eaefc8d49ac1991c63ad47a1`**. +[Persimmons launch run 1003](https://teahub.xyz/matcha/teahub/actions/runs/1003), +job **1005**, completed successfully through the existing independent Actions +operator. Provision/deploy/HTTP acceptance took **142.519 seconds**, finishing at +04:28:40 UTC. Every operator deploy was preceded by an explicit fetch with +HEAD = origin/main = source above, ahead 0 / behind 0 / dirty false. Initial base +was `6580c3380554bb1ca38269a1883ec86b7330bd99`. No delegation occurred. + +| Surface | Fly app / machine | Persistent volume | Image digest | +| --- | --- | --- | --- | +| `https://persimmons.app` | `persimmons-app` / `48e729dc5796e8`, release 28 | `vol_rkgqyxm5k2gxm064` (`persimmons_data`, existing 1 GiB) | `sha256:b414c01a4f70101fd39d35a81475f67494b69df4a38ef2d7234068d1dcc2ec96` | +| `https://git.persimmons.app` | `persimmons-gitea` / `0800251ae09308` | `vol_r68lo8o2kwy38md4` (`persimmons_gitea_data`, new 1 GiB) | `sha256:e395e81c7ceb898179fe14d277b4650dc31c74dd94784c05de14446d212847a4` | + +Both machines are in iad, one shared CPU and 512 MiB each. Dedicated Gitea image: +`registry.fly.io/persimmons-gitea:deployment-01M3TVDC6PTEF627XDF3VFW1WJ`; +product image: +`registry.fly.io/persimmons-app:deployment-01M3TVGTF7VZ6HXS8BM85CZ3AW`. +Image labels record the exact source above. + +Cloudflare DNS-only records for `git.persimmons.app`: A **66.241.124.151**, AAAA +**2a09:8280:1::1a2:6e84:0**, TTL 60. Valid HTTPS was independently exercised. +Existing apex records remain A **66.241.124.82**, AAAA +**2a09:8280:1::1a0:f8e1:0**. Gitea `/api/healthz` returned 200 and `pass` for both +`database:ping` and `cache:ping` in Actions and independently at 04:29:46 UTC. +Its public `/user/sign_up` returns a 200 notice saying registration is disabled; +it does not offer user-registration inputs. Bootstrap completed automatically; +no manual administrator step was required. + +Live API acceptance created **probe-e84a6b84644740fda0d1**, immutable Gitea ID **2**, +`is_admin=false`; its **probe-e84a6b84644740fda0d1/work-test**, repo ID **1**, was +independently read using that user's own credentials and verified private/empty. +Duplicate creation returned 409. Registration/login were 200, encrypted item +creation 201, persisted read after logout/login 200, deletion/logout 200. The +synthetic ciphertext and active session were removed. + +A fresh Chromium **153.0.8010.12** profile then exercised the actual live page, +without source/UI interception. Browser signup created +**browser-04780a06f4a43a72**, Gitea ID **3**, `is_admin=false`. The **+ New vault** +modal created **browser-04780a06f4a43a72/work-test**, repo ID **2**. Its name/owner +and “Private · Empty vault” card appeared below Master vault; an independent +Gitea API read confirmed owner ID 3, private=true, empty=true. Repeating the modal +showed the duplicate-repository conflict. + +The same browser linked and initialized Master vault using the real pinned +extension **0.4.8** (`kiolgjoohhgdohcafgelhikpgkbmckbg`), captured a synthetic login +at the real `https://wp.phly.cloud/wp-login.php`, locked/unlocked, explicitly filled +both fields with **zero POST submissions**, deleted the synthetic item, and +confirmed logout clears the extension connection. Homepage and popup lock state +agreed. The harness pregrants the exact Persimmons extension host permission; +it does not claim to click Chromium's native permission prompt. The disposable +profile was removed. No existing human account or provider credential was used. + +Local checks: **25 Python tests and 18 Node tests passed**. Actions repeated the +standalone host/Gitea/UX contract suite (**20 Python tests**) and **18 Node tests** +before provisioning. Coverage includes Gitea signup/login/session, duplicate user, +Master-vault continuity, private/empty/owner checks, duplicate/no-import behavior, +wrong-owner and replaced-repo rejection, request scoping, admin sign-in rejection, +credential non-disclosure, one-time bounded reset, package/crypto/capture/fill, +and the unchanged no-reveal MCP boundary. A pre-existing Node assertion expecting +removed Juice popup controls was updated to the already-accepted human-only popup; +legacy implementation and rotate/scoping documentation remain intact. + +Both product URLs returned current health/source and nonempty UI/assets. The live +app JS and extension ZIP match built candidate bytes exactly: + +- app.js SHA-256: `706c12437ce1e53723aa54e39c861632477923cf41e32a71c5bdf0b3319c6972` +- extension.zip SHA-256: `2ac21e6ac547073d8574ab99fd919645456085bcadf7a970a5ce06033d97097a` + +The reset ran only against Persimmons' disposable local identity/session/human +store when the new health check opened schema version 2. Teahub infrastructure, +Gitea data, and unrelated apps were not changed. The two synthetic non-admin users +and their empty private repositories remain as acceptance evidence. No Juice +implementation was added. This receipt-only follow-up does not require another +runtime deployment; the runtime/image source SHA is the one recorded above.