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

6.4 KiB
Raw Blame History

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_idhandle_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.onweb/index.html:417-419; (b) admin sidebar upgrade CTA #tier-banner-ctaweb/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.