Files
keysat-root/ROADMAP.md
T
Keysat 601ccea39c Adjudicate parked low-priority backlog items to verdicts
Ran the investigate→debate→judge pipeline over 4 parked ROADMAP items.

DROP:
- Design "structural" tier (palette consolidation): the rust-embedded admin
  SPA can't @import a shared file, so consolidation is a verbatim re-copy that
  doesn't remove the duplication it targets; the drift it guards is hypothetical.
- Design "token gaps" tier: manual churn across untested public surfaces, and
  the audit was partly mis-specified (#d4b985/#a6b7cf are token values, not
  hardcoded literals).

DO (low blast radius):
- Reframe the manual "Zaprite sandbox pass" for multi-profile webhook routing
  into an automated regression test — routing is a deterministic provider-id
  PK lookup with an anti-forgery backstop, but the path-keyed route has zero
  automated coverage on the money path.

ESCALATE:
- Zaprite contact dedup cache → lean DROP: cosmetic, unverified harm (Zaprite
  dedup-on-email is undocumented); fix is HIGH blast radius on the money path.
  Gated on one cheap sandbox check.
- Design "blocker" tier (3 gold-fill / pill-radius one-liners) → lean DO,
  pending an owner glance since they alter public/admin visuals.

Replaces the "harden Zaprite failure-body shapes" item (already satisfied for
non-2xx) with a bug the investigation surfaced: try_auto_charge_zaprite returns
Ok(true) on any 2xx, so a 200 carrying a FAILED/DECLINED/EXPIRED status
silently lapses the subscription. Elevated above the other parked payments
items; safe fail-safe fix needs no prod data.
2026-06-18 06:49:01 -05:00

67 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ROADMAP — Keysat
Longer-term backlog. Near-term state lives in `AGENTS.md` → Current state.
## Payments & subscriptions
- Per-profile SMTP override (schema fields exist from the keysat-smtp-emails plan; needs the form + send path).
- Rail-preference editing UI — only matters when two providers on one profile both serve the same rail; settable today via `PUT /v1/admin/merchant-profiles/:id/rail-preferences/:rail`.
- **Auto-charge silently lapses a subscription on a 200-with-failure response (money-path bug; elevated above the other parked payments items).** `try_auto_charge_zaprite` returns `Ok(true)` on *any* HTTP 2xx (`subscriptions.rs:1403-1410`), reading the order `status` only for a log line. If Zaprite returns 200 carrying a `FAILED`/`DECLINED`/`EXPIRED` order status, the daemon fires `auto_charge_initiated` and then waits for an `order.paid` webhook that never arrives — the subscription silently lapses, no error surfaced, the customer churns. Safe fix (no production data needed): treat any non-`PAID` terminal order status as not-success and fall through to the manual-pay path — a conservative fail-safe, ~10 lines + a mock-provider test. (Found during the 2026-06-17 adjudication; it replaces the old "harden Zaprite failure-body shapes" item, which was already satisfied for non-2xx responses — those route correctly to WARN + `auto_charge_failed` audit + webhook + manual-pay fallback.)
- Keysat-side dedup cache for Zaprite contacts (same buyer purchasing recurring twice can create duplicate Zaprite contacts). **Adjudicated 2026-06-17 → lean DROP.** Harm is cosmetic (duplicate rows in the operator's Zaprite contact list) and unverified — Zaprite's own dedup-on-email behavior is undocumented; the fix (new `zaprite_contacts` table + threading a DB handle into `ZapriteProvider`) is HIGH blast radius on the money path and could introduce a stale-cache misrouting bug. **Cheap check before dropping for good: buy twice with the same email on a Zaprite sandbox — one contact → drop outright; two → defer until real recurring revenue makes it worth it.**
## 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).
- **Onboarding doc-harness — Stage 2 (Path 2, regtest buyer-pays).** Gated on slices 35 above.
Stage 1 (Path 1, no payments) shipped `completed-clean` this session — harness at
`licensing-service-startos/onboarding-harness/`, record in its `STAGE1-RESULT.md`. Stage 2
reuses the harness but boots the fixture with `KEYSAT_SANDBOX_MODE` on, stands up a Dockerized
BTCPay regtest stack (bitcoind regtest + NBXplorer + Postgres + BTCPay) as additional
disposable infra, and grants the agent `merchant-onboard` + `payment_providers:write`. Goal:
the agent connects BTCPay (regtest) over the API and drives a test buyer payment that activates
a license, with zero master-key steps. The walkthrough must be explicitly labeled
regtest/test-network and must state that connecting a real mainnet wallet is the one
operator-reserved step **by design** (a key that can redirect funds stays with the human) — a
security feature, not a gap.
## Packaging & distribution
- Start9 Community Registry submission — a 2026-06-17 spec check found the wrapper passes the functional
criteria (manifest, interfaces, health check, backup/restore, BTCPay dep, actions). Remaining gap before
submission: add a `prepare.sh` to set up a clean Debian box for the first build (copy the one from
`hello-world-startos`), then run the on-box manual verification (install / backup / restore / logs).
Submission criteria themselves remain unpublished; reach out to Start9 when ready. (Icon-render and the
source-available license are intentionally not treated as blockers.)
## Licensing model
- Evaluate Elastic License v2 vs the current custom `LicenseRef-Keysat-1.0` (parked decision).
## Validation
- Re-test `KEYSAT_INTEGRATION.md` against a fresh downstream app to confirm a clean one-shot SDK integration.
- **Add an automated regression test for multi-profile webhook routing** (adjudicated 2026-06-17 → DO, low blast radius — replaces the parked "manual Zaprite sandbox pass"). The routing is a deterministic provider-id→profile primary-key lookup with an anti-forgery re-fetch backstop, so the manual sandbox ceremony isn't worth it — but the path-keyed route (`/v1/{provider}/webhook/:provider_id``handle_for_provider`) currently has zero automated coverage on the money path. Plan: in `tests/api.rs`, reuse the two-provider fixture (~:3958), POST a Settled webhook to `/v1/zaprite/webhook/{provider-A-id}`, assert only profile A settles (B untouched; an unknown path-id 404s). Existing mock seam, no external account, runs in `cargo test`. Effort S.
## Design (contract conformance)
The brand contract lives in `design/DESIGN.md` + `design/tokens.tokens.json` (distilled
2026-06-16 from the prior Claude Design system, archived in `design/_imports/`). A
`design-checker` audit (2026-06-16) found high fidelity overall. **Adjudicated 2026-06-17:**
the structural palette-consolidation and the token-gap nitpicks were **dropped** — the
consolidation can't actually remove the duplication it targets (the rust-embedded admin SPA
can't `@import` a shared file, so "consolidation" is a verbatim re-copy), and the token-gap
list was partly mis-specified by the audit. Only the three contract-"never" blockers survive.
**Blockers — approved to fix (adjudicated → lean DO; wants an owner glance since they change
public landing + admin visuals). Three reversible CSS one-liners:**
- Gold used as an actionable *fill* (contract: gold is accent/border only, never a fill).
(a) admin SPA `.featured-pill-toggle.on``web/index.html:417-419`; (b) admin sidebar
upgrade CTA `#tier-banner-cta``web/index.html:537`. Fix to navy-fill or gold-border/text.
- Primary buy CTA uses pill radius `999px` (contract: buttons are `r-md` 8px; pill is
badges-only) — `keysat-xyz-landing/index.html:390`. Set to 8px.