diff --git a/AGENTS.md b/AGENTS.md index 037b511..41e2ddd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,50 +114,44 @@ Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-keysat/ publish **`0.2.0:57`** — universal multi-arch (x86_64 + aarch64), GitHub release `v0.2.0-57`. Migrations 0020–0023; four SDKs published; `keysat.xyz` + `docs.keysat.xyz` deployed. -- **Live server `immense-voyage.local` still runs `:56`** — `:57` shipped to the registry - this session, but the box wasn't redeployed (that publish run predated this session's - change adding `make install` as `publish.sh` step 5). Run `make install` from - `licensing-service-startos/` once to catch the box up to `:57`; future `publish.sh` runs - now deploy to the live host automatically (best-effort, non-fatal). -- **Shipped this session as `:57`** (feature `d5885d1`, bump `069cf1e`; pushed - origin+gitea) — **`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, no master key). New `Role` variant - only — the catalog write scopes already existed and were enforced since `:55`; - `grants()` matches scope strings explicitly (never `:write` suffix) so it can't widen - into settings/payment/profile/webhook writes, and every master-only op stays behind - `require_admin`. Migration 0023 widens the `scoped_api_keys` role CHECK (no FKs → plain - rebuild). **Caveat**: covers catalog + manual issuance fully, but payment-provider - connect stays master-only, so buyer-paid purchase still needs a one-time operator step. - See `src/api/api_keys.rs`. + `:57` shipped the **`merchant-onboard`** scoped role (catalog + license self-serve, no + master key; `src/api/api_keys.rs`) — see git log for detail. +- **Live box `immense-voyage.local`**: `:57` was deployed this session via `make install` + (`start-cli package install` returned clean; StartOS applies the swap async — **not + independently confirmed**; verify the StartOS UI shows `0.2.0:57`). `publish.sh` now runs + `make install` as step 5, so future ships auto-deploy (best-effort, non-fatal). + +- **In progress — agent-payment-connect (phase 2)**. Approved spec: + `plans/agent-payment-connect-scope.md`. Lets a scoped key connect a BTCPay provider, but + ONLY on a sandbox daemon and ONLY for a non-mainnet network — never folded into a role + (a key that can repoint settlement is a fund-redirection key). + - **Foundation committed this session (`3afac07`, origin+gitea; NOT version-bumped)** — + slices 1–2 of 5: `Config.sandbox_mode` (env `KEYSAT_SANDBOX_MODE`, never API-settable; + surfaced in `/v1/admin/tier`); migration 0024 `scoped_api_keys.extra_scopes`; per-key + à-la-carte scopes (`GRANTABLE_EXTRA_SCOPES=["payment_providers:write"]`, granted via + `extra_scopes`, in NO role — `grants()` carves it out of full-admin's wildcard; + fail-closed parsing). Reviewer pass clean after fixing the full-admin-wildcard P1. + - **Pending — slices 3–5**: `require_provider_connect` gate (master→any; scoped+ + `payment_providers:write`→only if `sandbox_mode` AND non-mainnet); BTCPay OAuth wiring + (record `scoped_initiator` in `btcpay_authorize_state` at `start_connect`, network-check + at `finish_connect`, migration 0025); Zaprite stays master-only. **The BTCPay on-chain + address network detection MUST be validated against a live regtest box** before + shipping (classify address prefix `bc1`/`tb1`/`bcrt1`, fail-closed to mainnet; the + payment-method id is `BTC-CHAIN` vs `BTC` by version). - **Work queue (next, in order)**: - 1. Deploy `:57` to `immense-voyage.local` (`make install`) when ready. - 2. Operator data action (needs master key): grant `unlimited_merchant_profiles` to - Pro/Patron on the live master (see Open TODOs). - 3. 3 remaining multi-profile UIs (rail picker, per-profile SMTP, rail-pref editor) — - see ROADMAP. - 4. Split `audit:read` out of the blanket `:read` scope into its own tier (Open TODOs). + 1. Build gate slices 3–5 (above) — validate the BTCPay address fetch on regtest. + 2. Confirm `:57` is live on `immense-voyage.local` (StartOS UI). + 3. Operator data action (master key): grant `unlimited_merchant_profiles` to Pro/Patron. + 4. 3 multi-profile UIs + split `audit:read` (ROADMAP / Open TODOs). -- **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 (latent - since migration 0014; the new profile writer guards this correctly); no rate-limit on - `/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. +- **P2/P3 debt (unchanged)**: `set_product_entitlements_catalog` missing `rows_affected` + guard; no rate-limit on purchase/redeem (spoofable XFF); `422`/`415` plain-text not JSON; + `slug` unvalidated; `GET /v1/admin/products` 405 vs OpenAPI; dep advisories (`sqlx`→≥0.8.1, + `rustls-webpki`→≥0.103.12); no CI / fmt-clippy unenforced; field-naming drift; outbound + webhook SSRF; design-contract conformance (see ROADMAP). -- **P3+ (bulk or later decision)**: `/v1/purchase` 400 vs `/v1/btcpay/webhook` 503 for - the same no-provider cause; undocumented required `kind` on discount-codes; - field-naming drift (`license_id`/`id`, machines `key` vs `license_key`, - `redeem`/`purchase` `product` vs `validate` `product_slug`); migration - `_sqlx_migrations` allowlist foot-gun; 2 KB unauth Zaprite payload WARN-log; - outbound-webhook SSRF (operator-only); re-register master Zaprite webhook at the - path-keyed URL; registry icon non-render (platform limit); design-contract conformance - (see ROADMAP); optional fmt/prettier standalone commit. - -- **Tests/build**: `cargo check` + `npm run check` (tsc) clean (1 intentional - deprecation warning); full suite green — unit 10, api **57** (incl. the - merchant-onboard onboard-chain + master-only-denial test), subscriptions 7, upgrades 9, - worker 3, crosscheck 4, migrations 9 (through 0023). No new clippy warnings. FK +- **Tests/build**: `cargo check` + `npm run check` clean (1 intentional deprecation + warning); full suite green — lib unit **13**, api **59**, subscriptions 7, upgrades 9, + worker 3, crosscheck 4, migrations 9 (through 0024). No new clippy warnings. FK enforcement confirmed — sqlx pool sets `foreign_keys(true)` per connection. diff --git a/ROADMAP.md b/ROADMAP.md index c34a781..cbf147f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,6 +9,16 @@ Longer-term backlog. Near-term state lives in `AGENTS.md` → Current state. - Keysat-side dedup cache for Zaprite contacts (same buyer purchasing recurring twice can create duplicate Zaprite contacts). - Zaprite declined-card / expired-profile failure-body shapes are undocumented — harden `try_auto_charge_zaprite` once observed in production. +## Agent compatibility & scoped API keys + +- **Agent-delegable payment-provider connect** (approved, not urgent — see + `plans/agent-payment-connect-scope.md`). Add an à-la-carte `payment_providers:write` scope + (never bundled into `merchant-onboard`), gated by a daemon-level **sandbox-mode flag** as the + outer gate (production daemons reject scoped connect entirely) with a **network gate** inner + defense (regtest/testnet/signet only, fail-closed to mainnet). BTCPay network is derived from + an on-chain address prefix (no `server/info` field exists). Feeds the doc-harness Path 2 + (regtest buyer-pays). Ships after doc-harness Path 1. + ## Packaging & distribution - Start9 Community Registry submission — criteria are unpublished; contact Start9 directly when ready.