Finish Persims consolidation

This commit is contained in:
matcha committed 2026-10-04 02:57:00 +00:00
1 parent c755004684
commit 447eeaeba4
1 file changed
+144
+144
View File
@@ -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-<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](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.