Record scoped-keys + settle-tripwire work; document boundary and TODOs

Update Current state for the two P1 fixes done this session (source-only,
awaiting :55). Document the advisory settle-amount tripwire in payments.md. Add
Open TODOs: split audit:read into its own scope tier, and build the admin API-keys
management panel (both deferred to later sessions).
This commit is contained in:
Keysat
2026-06-13 00:10:53 -05:00
committed by Keysat
parent ffdb59aa8f
commit 6d4efc8a33
2 changed files with 69 additions and 56 deletions
+47 -52
View File
@@ -95,60 +95,54 @@ Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-licensi
- StartOS Community Registry submission criteria — Start9 hasn't published the
checklist; reach out directly when ready.
- Registry icon doesn't render in the StartOS marketplace (see `guides/startos-packaging.md`).
- Split `audit:read` out of the blanket `:read` scope into its own tier so a
Read-only scoped key can read dashboards/licenses but NOT the full audit log
(`api/api_keys.rs::Role::grants`). Deferred from the scoped-keys session.
- Build the admin SPA "API keys" management panel (create w/ role picker, list,
revoke) — backend is wired; UI deferred to a design-focused session.
## Current state (2026-06-13)
- **Live**: server `immense-voyage.local` runs daemon `0.2.0:54` (migrations
00200022 applied). Registry `registry.keysat.xyz` publishes `:54` too
(GitHub release `v0.2.0-54` cut; `files.keysat.xyz` serves the s9pk). Four SDKs
published; `keysat.xyz` + `docs.keysat.xyz` deployed.
00200022). Registry `registry.keysat.xyz` publishes `:54`; four SDKs published;
`keysat.xyz` + `docs.keysat.xyz` deployed. **Prod is still `:54` — this
session's two P1 fixes are committed to source but NOT yet built/installed/
published. Next release builds `:55`.**
- **`:52`/`:53` = multi-provider/merchant-profile model**: data model + backend
resolution shipped and audited sound; the resolution/CRUD query surface now has
test coverage. See `docs/guides/payments.md`.
- **Two payment-path fixes shipped 2026-06-13**: (a) `:53` fixed the `:52`
ambiguous-column bug that broke *every* paid purchase (daemon `31f4670`); (b)
`:54` fixed the **P0 Zaprite webhook-forgery** — settle now re-confirms against
the provider API before issuing (daemon `783372c`, bump `495fbbf`). Both built,
installed to prod, and published to the registry. Live purchase + settle paths
are sound.
- **GAP — multi-profile is non-functional end-to-end**: nothing in the shipped
app writes `products.merchant_profile_id` (the INSERT in
`create_product_with_currency` omits it; `update_product_with_currency` has no
field for it; the `Product` struct in `models.rs` doesn't even carry it). So
every product created post-migration stays on the default profile, and a Pro
operator can create extra profiles + attach providers but cannot route any
product's sales to them. The data model + resolver fully support it; only the
product→profile **write path** is missing. **This is the gating piece for
multi-profile** — see the scoped slice below.
- **Triage from `EVALUATION.md` (full-eval, 2026-06-13)** — P0/P1 = work queue,
P2 = known debt, P3+ = deferred. The report at repo root has file:line evidence;
it's tracked, so re-running full-eval overwrites it and `git log -- EVALUATION.md`
preserves prior runs.
resolution shipped and audited sound; resolution/CRUD query surface has tests.
Both `:54` P0s (provider-injection test seam; Zaprite webhook-forgery re-confirm)
remain fixed; live purchase + settle paths sound.
- **Work queue (P0/P1 — do first, in this order)**:
1. **SHIPPED in `:54` — Provider-injection test seam.** Added the
always-compiled `AppState::provider_override` + `provider_from_row` helper at
every resolution site; greened the two `paid_purchase_*` tests; deleted the
dead `payment_provider_preference_round_trip`. See `docs/guides/testing.md`.
2.**SHIPPED in `:54` — Zaprite webhook forgery fix.** `webhook.rs::handle_inner`
re-fetches `provider.get_invoice_status` and requires `Settled` before any
settle-derived action; acks 200 (no issue) when the provider is unreachable.
Two regression tests (forged-settle, provider-unreachable). api 47/47.
**Still open (auditor P1):** a literal paid-amount/currency check — needs a
trait change (`get_invoice_status` returns only a status enum). See
`docs/guides/payments.md`. **This is now the top remaining security item.**
3. **[P1] Scoped API keys (`ks_…`) are non-functional** — issuable but 403 on
every admin endpoint; the `require_admin``require_scope` migration was never
done. Finish it, or stop advertising/issuing them. `api/api_keys.rs:14`.
- **Then resume feature work**: the **productmerchant-profile picker** (the GAP
above — slice: add `merchant_profile_id` to the `Product` model + `repo.rs`
SELECT mapping; a `set_product_merchant_profile` follow-up writer mirroring
`set_product_entitlements_catalog`; the field on `CreateProductReq`/
`UpdateProductReq` applied post-write; a profile `<select>` from
`GET /v1/admin/merchant-profiles` in the create+edit product forms, shown only
when >1 profile; no migration), the 3 other deferred UIs (rail picker,
per-profile SMTP, rail-pref editor), and `unlimited_merchant_profiles` on
master Pro/Patron policies.
- **Done this session (source only, awaiting `:55`)** — the two open P1s:
1. **Settle-amount tripwire.** `get_invoice_status` now returns
`ProviderInvoiceSnapshot { status, amount }`; `audit_settle_amount` (shared by
webhook + reconcile issue paths) WARNs + writes an `invoice.amount_mismatch`
audit row on drift, then **issues anyway** (advisory, not a gate — a hard gate
would fight BTCPay payment tolerance). SAT-only: skips non-SAT (fiat sub
renewals) and `None`. Reviewed (caught + fixed a fiat-renewal false-positive).
See `docs/guides/payments.md`.
2. **Scoped API keys wired.** 58 admin endpoints migrated `require_admin`
`require_scope`; 12 sensitive ones stay master-only (issuer key, provider
connect/disconnect, set-password, api-key CRUD, db-info, operator-name,
per-license tier change). `require_scope` re-exported from `api::admin`. Role
boundary tests added. Boundary documented in `api/api_keys.rs` module doc.
- **GAP — multi-profile still non-functional end-to-end**: nothing writes
`products.merchant_profile_id` (INSERT in `create_product_with_currency` omits
it; `update_product_with_currency` has no field; `Product` in `models.rs` lacks
it). Resolver fully supports it; only the product→profile **write path** is
missing. **Gating piece for multi-profile.**
- **Work queue (next, in order)**:
1. **product→merchant-profile picker** (the GAP — add `merchant_profile_id` to
`Product` + `repo.rs` SELECT; `set_product_merchant_profile` writer mirroring
`set_product_entitlements_catalog`; field on `CreateProductReq`/
`UpdateProductReq` applied post-write; profile `<select>` from
`GET /v1/admin/merchant-profiles`, shown only when >1 profile; no migration).
2. 3 other deferred UIs (rail picker, per-profile SMTP, rail-pref editor);
`unlimited_merchant_profiles` on master Pro/Patron policies.
3. Deferred this session (now in Open TODOs): split `audit:read` out of the
blanket `:read` scope; build the admin "API keys" management SPA panel.
- **Known debt (P2 — schedule, not urgent)**: no rate-limit on `/v1/purchase` +
`/v1/redeem`; rate-limit bucket keys on spoofable `X-Forwarded-For` (bypass
@@ -171,7 +165,8 @@ Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-licensi
webhook at the path-keyed URL; registry icon non-render (known platform limit);
optional fmt/prettier standalone commit.
- **Tests/build**: `cargo check` clean (1 intentional deprecation warning); api
43 pass / 3 known-fail (now tracked in the work queue above), other suites
green. FK enforcement **confirmed** — sqlx pool sets `foreign_keys(true)` per
connection (`db/mod.rs`). CI/fmt status is in Known debt.
- **Tests/build**: `cargo check` clean (1 intentional deprecation warning); full
suite green — api **54** (incl. new settle-tripwire + scoped-key role-boundary
tests), subscriptions 7, upgrades 9, worker 3, crosscheck 4, migrations 9. No new
clippy warnings. FK enforcement **confirmed** — sqlx pool sets `foreign_keys(true)`
per connection (`db/mod.rs`). CI/fmt status is in Known debt.