Update Current state: 0.2.0:57 shipped (merchant-onboard role)

This commit is contained in:
Keysat
2026-06-16 19:25:24 -05:00
parent 62db8c81f3
commit d4ad8c3fa7
+45 -69
View File
@@ -110,77 +110,53 @@ Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-keysat/
## Current state (2026-06-16) ## Current state (2026-06-16)
- **Live**: registry `registry.keysat.xyz` publishes **`0.2.0:56`** — universal - **Live (registry/canonical)**: `registry.keysat.xyz` + `files.keysat.xyz/keysat.s9pk`
multi-arch, GitHub release `v0.2.0-56`, canonical s9pk at publish **`0.2.0:57`** — universal multi-arch (x86_64 + aarch64), GitHub release
`files.keysat.xyz/keysat.s9pk`. Migrations 00200022; four SDKs published; `v0.2.0-57`. Migrations 00200023; four SDKs published; `keysat.xyz` +
`keysat.xyz` + `docs.keysat.xyz` deployed. **Live server `immense-voyage.local` `docs.keysat.xyz` deployed.
now runs `:56`** — deployed via `make install` this session and verified: public - **Live server `immense-voyage.local` still runs `:56`** — `:57` was published to the
`/v1/products` 200, and `/v1/admin/tier` with a read-only scoped key flipped registry this session but NOT `make install`-deployed to the box (`publish.sh` doesn't
403→200, confirming the `:55` `require_scope` rollout is now active on the box. touch the live host). Run `make install` from `licensing-service-startos/` to put the
(`:55` shipped scoped keys, the settle-amount tripwire, and all 4 StartOS new role on the live server when ready.
submission blockers; `:56` adds the product→merchant-profile write path — see git log.) - **Shipped this session as `:57`** (feature `d5885d1`, bump `069cf1e`; pushed
- **Shipped + RELEASED this session as `:56` (daemon feature `b088bfc`, origin+gitea) — **`merchant-onboard` scoped-key role** for least-privilege self-serve
version-bump `6b02992`, doc-fix `d2846ac`; pushed origin+gitea)** — onboarding: read + `products:write` + `policies:write` + `licenses:write` (create
**product→merchant-profile write path, closing the multi-profile GAP.** product → define policies/tiers → issue licenses, no master key). New `Role` variant
`Product.merchant_profile_id` + all 4 product SELECTs + `row_to_product`; new only — the catalog write scopes already existed and were enforced since `:55`;
`repo::set_product_merchant_profile` (validates profile exists → 404, not FK 500); `grants()` matches scope strings explicitly (never `:write` suffix) so it can't widen
threaded through `CreateProductReq` (post-write) + `UpdateProductReq` into settings/payment/profile/webhook writes, and every master-only op stays behind
(double-Option, `Some(None)` clears to default); admin SPA profile `<select>` `require_admin`. Migration 0023 widens the `scoped_api_keys` role CHECK (no FKs → plain
shown only when >1 profile. Mirrors the entitlements-catalog post-write pattern. rebuild). **Caveat**: covers catalog + manual issuance fully, but payment-provider
Reviewer pass: approved, no blockers. **Multi-profile is now functional connect stays master-only, so buyer-paid purchase still needs a one-time operator step.
end-to-end** (resolver was already complete; only the write path was missing). See `src/api/api_keys.rs`.
See `docs/guides/payments.md`.
- **Committed this session (`d5885d1`, pushed origin+gitea; NOT yet
version-bumped/released)** — **new `merchant-onboard` scoped-key role** for
least-privilege self-serve onboarding: read + `products:write` +
`policies:write` + `licenses:write` (create product → define policies/tiers →
issue licenses) without the master key. The catalog write scopes already
existed and were enforced; only the `Role::grants` expansion was missing, so
this is a new `Role` variant, not a scope-model change. `grants()` matches
scope strings **explicitly** (never by `:write` suffix) so it can't widen into
settings/payment/merchant-profile/webhook writes; every master-only op stays
behind `require_admin` and is unreachable; tier caps still bound it. Migration
0023 rebuilds `scoped_api_keys` to widen the role CHECK (no FKs → plain
copy/drop/rename). **Caveat for the doc-onboarding harness/marketing**: this
credential covers catalog + manual license issuance fully, but **cannot connect
a payment provider** (master-only by design), so the buyer-paid purchase flow
still needs a one-time operator step. See `src/api/api_keys.rs`.
- **Work queue (next, in order)**: - **Work queue (next, in order)**:
1. 3 remaining multi-profile UIs (rail picker, per-profile SMTP, rail-pref 1. Deploy `:57` to `immense-voyage.local` (`make install`) when ready.
editor). 2. Operator data action (needs master key): grant `unlimited_merchant_profiles` to
2. Operator data action (now unblocked — `:56` is live): grant Pro/Patron on the live master (see Open TODOs).
`unlimited_merchant_profiles` to Pro/Patron on the master (see Open TODOs). 3. 3 remaining multi-profile UIs (rail picker, per-profile SMTP, rail-pref editor) —
3. Deferred (now in Open TODOs): split `audit:read` out of the blanket `:read` see ROADMAP.
scope into its own tier. 4. Split `audit:read` out of the blanket `:read` scope into its own tier (Open TODOs).
- **Discovered this session (P2, unfixed)**: `set_product_entitlements_catalog` - **P2 (unfixed, deferred pending user's call)**: `set_product_entitlements_catalog`
has no `rows_affected` guard — a bad product-id silently 200s with stale data has no `rows_affected` guard — a bad product-id silently 200s with stale data (latent
(latent since migration 0014; the new profile writer guards this correctly). since migration 0014; the new profile writer guards this correctly); no rate-limit on
One-line fix, deferred pending the user's call. `/v1/purchase`+`/v1/redeem` (bucket keys on spoofable `X-Forwarded-For`); `422`/`415`
return plain-text not JSON (breaks SDK `JSON.parse`); product `slug` unvalidated;
`GET /v1/admin/products` 405 vs OpenAPI; dep advisories (`sqlx`→≥0.8.1
RUSTSEC-2024-0363, `rustls-webpki`→≥0.103.12); no CI, fmt/clippy/prettier unenforced.
- **Known debt (P2 — schedule, not urgent)**: no rate-limit on `/v1/purchase` + - **P3+ (bulk or later decision)**: `/v1/purchase` 400 vs `/v1/btcpay/webhook` 503 for
`/v1/redeem`; rate-limit bucket keys on spoofable `X-Forwarded-For` (bypass the same no-provider cause; undocumented required `kind` on discount-codes;
conditional on whether the StartOS proxy rewrites XFF — unverified); `422`/`415` field-naming drift (`license_id`/`id`, machines `key` vs `license_key`,
errors return plain-text not JSON (breaks SDK `JSON.parse`); product `slug` has `redeem`/`purchase` `product` vs `validate` `product_slug`); migration
no validation (empty/300-char/meta chars stored); `GET /v1/admin/products` `_sqlx_migrations` allowlist foot-gun; 2 KB unauth Zaprite payload WARN-log;
returns 405 though OpenAPI documents it; dep advisories (`sqlx`→≥0.8.1 outbound-webhook SSRF (operator-only); re-register master Zaprite webhook at the
RUSTSEC-2024-0363, `rustls-webpki`→≥0.103.12); no CI + fmt/clippy/prettier path-keyed URL; registry icon non-render (platform limit); design-contract conformance
unenforced. (The 4 StartOS submission blockers are resolved and shipped in `:55`.) (see ROADMAP); optional fmt/prettier standalone commit.
- **Deferred (P3+ — bulk or later decision)**: `/v1/purchase` 400 vs - **Tests/build**: `cargo check` + `npm run check` (tsc) clean (1 intentional
`/v1/btcpay/webhook` 503 for the same no-provider cause; undocumented required deprecation warning); full suite green — unit 10, api **57** (incl. the
`kind` on discount-codes; field-naming drift (`license_id`/`id`, machines `key` merchant-onboard onboard-chain + master-only-denial test), subscriptions 7, upgrades 9,
vs `license_key`, `redeem`/`purchase` `product` vs `validate` `product_slug`); worker 3, crosscheck 4, migrations 9 (through 0023). No new clippy warnings. FK
migration self-heal `_sqlx_migrations` allowlist foot-gun; 2 KB unauth Zaprite enforcement confirmed — sqlx pool sets `foreign_keys(true)` per connection.
payload WARN-log; outbound-webhook SSRF (operator-only); re-register the master
Zaprite 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); full
suite green with the merchant-onboard role — unit 10, api **57** (incl. the
merchant-onboard onboard-chain + master-only-denial test), subscriptions 7,
upgrades 9, worker 3, crosscheck 4, migrations 9 (now through 0023). 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.