Files
app/docs/persimmons-gitea-reset.md
2026-10-04 02:57:00 +00:00

9.6 KiB

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-<id>, 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, Gitea create user API, Gitea create repository on behalf of a user, and Fly Machines API.

Accepted live deployment — 2026-10-01

Done. Deployed source 2f8fcc8208205b30eaefc8d49ac1991c63ad47a1. Persimmons launch run 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.