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.