Finish Persims consolidation
This commit is contained in:
1 parent
c755004684
commit
447eeaeba4
1 file changed
+144
@@ -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.
|
||||||
Reference in new issue
Block a user