Update full-eval report (2026-06-18): no P0, three P1s
This commit is contained in:
+66
-52
@@ -1,72 +1,86 @@
|
||||
# Evaluation — Keysat (licensing-service-startos) — 2026-06-13
|
||||
# Evaluation — Keysat — 2026-06-18
|
||||
|
||||
Intent: a self-hosted, Bitcoin-native software-licensing service that runs as a StartOS 0.4.x package — a Rust/axum/sqlx+SQLite daemon with Ed25519 license signing plus a TypeScript StartOS wrapper — recently extended with a multi-merchant-profile, multi-provider (BTCPay required, Zaprite optional) payment model.
|
||||
Intent: a self-hosted, Bitcoin-native software-licensing service that runs as a StartOS 0.4.x package (Rust/axum/sqlx+SQLite daemon with Ed25519 license signing + a TypeScript StartOS wrapper), with four wire-compatible client SDKs (TS, Rust, Python, Go), a public landing/docs site, and BTCPay Server (required) + Zaprite (optional) Bitcoin payment providers.
|
||||
|
||||
Evaluation target: `licensing-service-startos/` (the daemon + StartOS wrapper). The SDK repos, public sites, and `plans/` were out of scope.
|
||||
|
||||
Agents run: evaluator, security-auditor, exerciser, start9-spec-checker (all completed). reviewer **skipped** — the guide runs it only on uncommitted changes, and the daemon tree was clean at evaluation time.
|
||||
Agents run: evaluator, security-auditor, exerciser, doc-auditor, start9-spec-checker. **reviewer skipped** — working tree is clean (no uncommitted diff to review).
|
||||
|
||||
## Verdict
|
||||
|
||||
Keysat substantially achieves its intent: the cryptographic licensing core (offline-verifiable LIC1 keys, Ed25519, cross-language-tested) is reference-grade, the payment-provider trait is well-factored, the SQL surface is injection-clear despite runtime-prepared queries, and the security fundamentals (Argon2id, constant-time HMAC/compare, 256-bit sessions, generic-error recovery, FK enforcement confirmed) are above the bar for a project this young. The one release-blocking defect is the **Zaprite settle-webhook: it performs no authentication of any kind and issues a signed license off a buyer-visible order id — anyone who starts a real Zaprite checkout can forge a "paid" callback and mint licenses without paying** (the `externalUniqId` "trust anchor" the code comments describe is never actually read in the webhook path). Behind that sits a systemic test gap — the production purchase/settle path has no integration-test seam, which is exactly how the `:52` 500-on-every-purchase bug shipped — and a cluster of API-hygiene and StartOS-submission issues. None of the deeper architecture is rotten; the work is hardening a sound core, not rebuilding it.
|
||||
Keysat substantially achieves its stated intent and is mature, security-conscious work well above typical solo-project quality on the surfaces that handle money and crypto. The daemon builds clean, the full test suite is green (≈117–131 tests across 8 suites), the LIC1 wire format is genuinely implemented byte-identically across the daemon and all four SDKs, and the payment/settle path is built fail-closed with real anti-forgery thinking (webhooks re-fetch authoritative provider status before issuing anything — this defeats Zaprite's unsigned-webhook forgery). No agent found a P0; the security auditor found no P0/P1. The headline risks are all below release-blocking: two shipped developer-experience breakages (the Rust SDK's published examples/README import the wrong crate name so they don't compile, and the cross-language crosscheck harness has a hardcoded machine-specific path), a cluster of P2 hardening/hygiene items (X-Forwarded-For rate-limit bypass, unpatched dependency advisories, admin UI co-located with the public API, runtime-prepared SQL with no compile-time safety net), and the still-missing `prepare.sh` that blocks Start9 community-registry submission. The live `0.2.0:60` deployment is sound; the work below is polish, hardening, and unblocking the registry milestone.
|
||||
|
||||
## Cross-referenced findings
|
||||
|
||||
- **Zaprite webhook forgery (P0).** Flagged by BOTH the evaluator (as P2) and the security-auditor (escalated to **P0** after tracing it end-to-end: public route → no signature → `externalUniqId` never read → no amount check → no `pending`-state precondition → `webhook.rs:194` issues a signed license). One finding, two agents, severity resolved to P0 (funds loss). `payment/zaprite/provider.rs:234-362`, `api/webhook.rs:62-196`, route `api/mod.rs:429-430`.
|
||||
- **Untested production purchase path (P1).** Evaluator named it the #1 issue (mock injects into the bypassed legacy `state.payment` singleton at `tests/api.rs:457`, not the real `resolve_provider_for_profile_rail` at `purchase.rs:482`); the exerciser independently reported it could not exercise the live payment/settle path at all (medium confidence). Same gap, two kinds of evidence. This is also the root of the two `paid_purchase_*` red tests.
|
||||
- **FK enforcement (resolved, not a finding).** A prior "latent/unchecked" worry; the security-auditor and daemon-architecture both confirm the sqlx pool sets `foreign_keys(true)` per connection (`db/mod.rs`). Closed.
|
||||
- **Rust SDK wrong crate name** — corroborated by two agents. The **exerciser** proved it breaks the build (`cargo build --examples` → `E0432: unresolved import 'licensing_client'`; crate is `keysat-licensing-client` → module `keysat_licensing_client`). The **doc-auditor** found the same wrong import propagated through `licensing-client-rust/README.md:26,39,56`, `src/lib.rs:20`, both `examples/*.rs`, and the public docs page `keysat-docs/integrate.html:130`. ONE finding, two kinds of evidence (build break + doc propagation) → **P1**.
|
||||
- **`unlimited_merchant_profiles` entitlement status** — the **doc-auditor** found `docs/guides/licensing-tiers.md:43-45` AND the code comment `src/api/tier.rs:234-237` both say it "still needs adding" to Pro/Patron policies, while AGENTS.md Current state says it's "confirmed live." Internal contradiction that needs a live `GET /v1/products/keysat/policies` check to resolve → **P2**.
|
||||
- **Health-check / readiness** — the **start9-spec-checker** noted the StartOS health check is port-listening only (flips green when 8080 binds, before migrations finish); the **exerciser** noted the daemon actually exposes a richer `GET /healthz` (and `/health` 404s). Same underlying surface → one **P3**.
|
||||
- **StartOS actions surface** — the **start9-spec-checker** found 21 registered actions vs the **doc-auditor**'s confirmation that `instructions.md`/README describe only ~4. Doc drift + extra test surface for Start9 review → **P3**.
|
||||
- **Test-count drift** — evaluator counted ≈131, exerciser ran 117, doc-auditor found the `api` suite at 65 vs the documented 54. All green; the *documented* counts in `docs/guides/testing.md:57` are stale → **P3** (doc only).
|
||||
|
||||
## Priority queue
|
||||
|
||||
- **[P0]** Zaprite settle-webhook is unauthenticated — forged `order.change`/`status=PAID` with a buyer-visible order id mints a free signed license; no amount or state check — `payment/zaprite/provider.rs:234-362` + `api/webhook.rs:62-196` — security-auditor (P0) + evaluator (P2)
|
||||
- **[P1]** Production purchase/settle path has no integration-test seam; mock targets the dead `state.payment` singleton, not `resolve_provider_for_profile_rail` — `payment/mod.rs:177`, `tests/api.rs:457`, `purchase.rs:482` — evaluator + exerciser
|
||||
- **[P1]** Settle handler issues without verifying paid amount/currency or requiring `pending` state (underpaid/replayed invoices honored at full entitlement; BTCPay forgery blocked by HMAC but replay is a no-op only after first issuance) — `api/webhook.rs:121-194` — security-auditor
|
||||
- **[P1]** Scoped API keys (`ks_…`) are advertised + issuable but return 403 on every admin endpoint — the `require_admin`→`require_scope` migration was never done — `api/api_keys.rs:14` — exerciser
|
||||
- **[P2]** `/v1/purchase` and `/v1/redeem` have no rate limiting (invoice-spam / upstream-provider exhaustion; unthrottled `free_license` code guessing) — `api/mod.rs:343-345` — security-auditor
|
||||
- **[P2]** Per-IP rate-limit bucket keys on attacker-controllable `X-Forwarded-For`; bypassable if the daemon's shared `0.0.0.0:8080` listener is reachable without the StartOS proxy rewriting XFF (unverified) — `recover.rs:65-70`, `validate.rs:95-111`, `admin.rs:56-67` — security-auditor
|
||||
- **[P2]** All `422`/`415` responses return naked axum plain-text rejection strings instead of the JSON error envelope — SDK consumers that `JSON.parse()` a 422 throw — every JSON endpoint — exerciser
|
||||
- **[P2]** Product `slug` has no validation — empty string, 300-char, and SQL-meta characters all stored as valid slugs; empty-slug product pollutes the public catalog — `POST /v1/admin/products` — exerciser
|
||||
- **[P2]** `GET /v1/admin/products` returns 405 though the served OpenAPI documents it (`products:read`); only `post` is wired — `api/mod.rs:431` — exerciser
|
||||
- **[P2]** Dependency advisories: `sqlx 0.7.4` (RUSTSEC-2024-0363, fixed ≥0.8.1), `rustls-webpki 0.101.7` (CRL panic / name-constraint bypass, fixed ≥0.103.12); `rsa` reachable only via unused MySQL backend; `paste`/`rustls-pemfile` unmaintained — `cargo audit` — security-auditor
|
||||
- **[P2]** StartOS submission blocker: `instructions.md` absent (spec says build should fail; `start-cli` built silently anyway) — start9-spec-checker
|
||||
- **[P2]** StartOS submission blocker: `packageRepo` is a dead link — manifest points to `…/keysat-startos` (404); real repo is `…/keysat` — `startos/manifest/index.ts` — start9-spec-checker
|
||||
- **[P2]** StartOS submission blocker: `docsUrls` dead link — points to `/docs/INTEGRATION.md`; file is at `/licensing-service/docs/INTEGRATION.md` — `startos/manifest/index.ts` — start9-spec-checker
|
||||
- **[P2]** StartOS submission blocker: manifest declares `aarch64` but only x86_64 is published (Start9 review tests on Pi-class hardware) — `publish.sh` / manifest `arch` — start9-spec-checker (corroborates existing AGENTS.md TODO)
|
||||
- **[P2]** No CI; `cargo test`/`clippy`/`fmt` all unenforced (56 files differ from `fmt`); the only release gate is `publish.sh` — for a payment daemon the untested suite is the load-bearing weakness behind the P0/P1s — `docs/guides/testing.md` — evaluator
|
||||
- **[P3]** Dead test `payment_provider_preference_round_trip` inserts into the dropped `btcpay_config`/`zaprite_config` tables — delete it — `tests/api.rs:1224` — evaluator + exerciser
|
||||
- **[P3]** `/v1/purchase` (400) and `/v1/btcpay/webhook` (503) return different status/error codes for the same "no provider configured" condition — exerciser
|
||||
- **[P3]** `POST /v1/admin/discount-codes` requires an undocumented `kind` field → opaque 422 — exerciser
|
||||
- **[P3]** Field-naming inconsistencies across related endpoints: `license_id` vs `id`; machines use `key` vs documented `license_key`; `redeem`/`purchase` use `product` vs `validate`'s `product_slug` — exerciser (Surprises)
|
||||
- **[P3]** Migration self-heal auto-deletes `_sqlx_migrations` rows on checksum drift, gated only by a hand-maintained allowlist — foot-gun as the list grows — `db/mod.rs` / migration 0009 — evaluator (Surprise) + security-auditor
|
||||
- **[P3]** Zaprite `validate_webhook` WARN-logs up to 2 KB of raw unauthenticated payload — log-flood / log-injection from a public endpoint — `provider.rs:262-278` — security-auditor
|
||||
- **[P3]** Outbound webhook delivery POSTs to operator-supplied URLs with no SSRF allow-list (operator-only input; a stolen admin token could pivot to internal hosts) — `webhooks.rs:150-156` — security-auditor
|
||||
- **[P3]** Stale comment in `startos/versions/v0.2.0.ts:3-4` says "NOT YET WIRED INTO versions/index.ts" but it is wired as `current` at `:53` — evaluator
|
||||
**P0** — none.
|
||||
|
||||
- [P1] Rust SDK examples + README + `integrate.html` import `licensing_client` but the crate is `keysat-licensing-client`; examples don't compile — `licensing-client-rust/{README.md:26,39,56, src/lib.rs:20, examples/offline_verify.rs:10, examples/online_validate.rs:9}`, `keysat-docs/integrate.html:130` — exerciser + doc-auditor
|
||||
- [P1] Cross-language crosscheck harness has a hardcoded dev-machine path `/sessions/hopeful-determined-edison/...`, breaks on every other machine — `tests/crosscheck/run_ts.mjs:9` — exerciser
|
||||
- [P1] No `prepare.sh` for the clean-Debian first build → blocks Start9 community-registry submission (already tracked in AGENTS.md/ROADMAP; not blocking the live deploy) — repo root of `licensing-service-startos/` — start9-spec-checker
|
||||
- [P2] `X-Forwarded-For` is trusted verbatim as the rate-limit bucket key on brute-forceable endpoints; rotating XFF defeats login/recover/validate throttles (exploitability hinges on whether the StartOS front proxy overwrites XFF) — `api/auth.rs:137`, `api/recover.rs:65-70`, `api/admin.rs:63-68`, `api/validate.rs:95-98` — security-auditor
|
||||
- [P2] Dependency advisories with fixes available: `sqlx 0.7.4` (RUSTSEC-2024-0363), `rustls-webpki 0.101.7` (RUSTSEC-2026-0098/0099/0104), wrapper `fast-xml-parser` via start-sdk (GHSA-5wm8-gmm8-39j9 high) — `licensing-service/Cargo.lock`, wrapper `package-lock.json` — security-auditor
|
||||
- [P2] Admin UI + `/v1/admin/*` share the single `:8080` interface with the public `/v1/validate` + `/buy` surfaces (path-routed only); operator can't network-isolate the admin surface — `startos/interfaces.ts:22-77`, `Dockerfile:95` — security-auditor
|
||||
- [P2] Admin webhook-endpoint registration accepts arbitrary URLs incl. loopback IPs and `file://` scheme (no allowlist on the `reqwest` worker); admin-gated but `file://` should never be accepted — `webhooks.rs:106` — exerciser
|
||||
- [P2] `db/repo.rs` (+ `subscriptions.rs`) use runtime-prepared `sqlx::query(&format!(...))` with no compile-time column checking; this class already shipped a prod regression (every paid purchase 500'd on `:52`) — `db/repo.rs:662,718`, `subscriptions.rs:180` — evaluator
|
||||
- [P2] `rate_buckets` grows unbounded — one row per distinct client IP on a public endpoint, with no reaper (sessions/redemptions have reapers; rate buckets don't) — `rate_limit.rs:63-78`, cf. `main.rs:151,174` — evaluator
|
||||
- [P2] No CI enforces anything; tree has never been `cargo fmt`'d (56 daemon files differ, 37 wrapper files differ from prettier); green depends on the operator remembering to run the suite before `publish.sh` — `docs/guides/testing.md:22-30` — evaluator
|
||||
- [P2] `unlimited_merchant_profiles` live-status contradiction between docs/code ("still needs adding") and AGENTS.md ("confirmed live") — `docs/guides/licensing-tiers.md:43-45`, `src/api/tier.rs:234-237` vs AGENTS.md — doc-auditor
|
||||
- [P3] Go SDK alone accepts trailing payload bytes; daemon + Rust + TS + Python all reject them — divergence in the "wire-compatible" contract (not a forgery hole) — `licensing-client-go/keysat.go:192-193` — evaluator
|
||||
- [P3] Fingerprint-bound payload check runs *after* machine-row activation + `machine.activated` webhook dispatch; a bound-mismatch creates a row + fires a webhook before rejecting — `validate.rs:412` vs `:326-345` — evaluator
|
||||
- [P3] Rate-limit consume is read-then-write (non-atomic TOCTOU); concurrent requests to one bucket can both pass — `rate_limit.rs:31-78` — evaluator
|
||||
- [P3] Buyer-controlled `req.redirect_url` passed unvalidated to BTCPay as post-purchase `redirectURL` (self-targeted open redirect) — `api/purchase.rs:513` — security-auditor
|
||||
- [P3] `html_escape` omits `'` (safe only because templates use double-quoted attributes) — `api/buy_page.rs:1536` — security-auditor
|
||||
- [P3] `audit:read` still folded into the blanket `:read` scope (a read-only key can read the full audit log) — `api/api_keys.rs` (already in AGENTS.md TODOs) — security-auditor
|
||||
- [P3] Container runs as root; justified by StartOS LXC isolation but a non-root user + chowned volume is stronger — `Dockerfile:70-95` — security-auditor
|
||||
- [P3] `run_migrations_with_self_heal` deletes `_sqlx_migrations` rows on checksum mismatch to avoid crash-loops — pragmatic but quietly self-modifying; worth a second look — `db/mod.rs:44` — evaluator (Surprise)
|
||||
- [P3] Health check is port-listening only and flips green before migrations finish; daemon exposes richer `GET /healthz` (and `/health` 404s, could confuse probes) — `startos/main.ts:237` — start9-spec-checker + exerciser
|
||||
- [P3] 21 registered StartOS actions vs ~4 documented in `instructions.md`/README; extra (legacy) surface Start9 will exercise during review — `startos/actions/`, `instructions.md` — start9-spec-checker + doc-auditor
|
||||
- [P3] Source-available license (`LicenseRef-Keysat-1.0`, not OSI) may trigger a Start9 policy question; source is public but redistribution is restricted — confirm with submissions@start9.com — start9-spec-checker
|
||||
- [P3] Universal s9pk publish path never exercised end-to-end ("confirm the registry index lists both arches on first publish") — `docs/guides/startos-packaging.md:63` — start9-spec-checker
|
||||
- [P3] Built manifest emits `donationUrl: null`; confirm Start9 registry ingest tolerates a null vs an omitted field — start9-spec-checker
|
||||
- [P3] ROADMAP.md:8 still lists the Zaprite auto-charge silent-lapse as open; fix shipped in `:60` — `ROADMAP.md:8` vs `src/subscriptions.rs:1408-1428` — doc-auditor
|
||||
- [P3] BUILDING.md stale: "rust:1.75-slim-bookworm" (now `1.88`) and "Node 20+" (now Node 22) — `BUILDING.md:15,20` vs `Dockerfile`/`startos-packaging.md:14`/`package.json` — doc-auditor
|
||||
- [P3] Landing-page footer links `docs.keysat.xyz/changelog` but no `changelog.html` exists — `keysat-xyz-landing/index.html:1072` — doc-auditor
|
||||
- [P3] README action mislabeled "Show credentials"; actual action name is "Show admin API key" — `licensing-service-startos/README.md:160` vs `startos/actions/showCredentials.ts:21` — doc-auditor
|
||||
- [P3] `PORTING_SDK_TO_NEW_LANGUAGES.md` describes Python/Go as "v0.2 goals" (both shipped) and calls the Rust crate `licensing-client` (renamed) — `PORTING_SDK_TO_NEW_LANGUAGES.md:1-13` — doc-auditor
|
||||
- [P3] `docs/guides/testing.md:57` test counts stale (54 vs actual 65 in `api` suite) — doc-auditor
|
||||
- [P3] `ZAPRITE_INTEGRATION_SPEC.md:401` references receipt emails (email plan dropped); add a SUPERSEDED banner — doc-auditor
|
||||
- [P3] `MASTER_KEYPAIR_PROCEDURE.md:199-210` references a "v0.3 rolling-rotation flow" with no documented timeline; verify still planned or move to ROADMAP — doc-auditor
|
||||
- [P3] Max int64 price (9.2e18 sats) accepted/stored with no upper bound — exerciser (Surprise)
|
||||
- [P3] `validate` silently ignores an unknown `product_id` body field (only `product_slug` is checked) — a dev sending `product_id` gets a false `ok:true` — `api/validate.rs` — exerciser (Surprise)
|
||||
- [P3] Deprecated `SETTING_ACTIVE_PROVIDER` constant emits a build warning on every compile — evaluator + exerciser
|
||||
- [P3] CORS `allow *` on all endpoints incl. admin (by design for SDK callers; relies on key auth + TLS) — exerciser
|
||||
|
||||
## Scorecard
|
||||
|
||||
The evaluator's lens table, with one adjustment from cross-agent evidence:
|
||||
|
||||
| Lens | Score /5 | Note |
|
||||
|------|----------|------|
|
||||
| Architecture | 4 | Clean trait + per-profile resolver; legacy `state.payment` singleton coexists and is what tests inject into — the fork behind the test gap. |
|
||||
| Security | **3** (was 4) | **Adjusted down** by the security-auditor's P0 forgery + P1 missing-amount-check (funds loss). Fundamentals remain 4–5 (Argon2id, HMAC, injection-clear, FK-enforced); the Zaprite P0 is the pull. Zaprite is optional, which bounds blast radius. |
|
||||
| Performance | 4 | WAL + 8-conn pool, indexed lookups, bounded workers; no load test run. |
|
||||
| Testing | 3 | Cross-language LIC1 fixtures are a real strength; the production purchase/settle path is untested — corroborated by the exerciser's inability to reach it and the shipped-but-broken scoped-API-key feature. |
|
||||
| Code quality | 5 | Disciplined dynamic SQL (all `.bind()`, const-only interpolation); honest comments. Caveat (not score-changing): the exerciser found absent input validation (slug) and plain-text error leaks. |
|
||||
| Documentation | 5 | Scoped guides + wire-format spec are reference-grade and candid (testing.md names its own known failures). |
|
||||
| Lens | Score /5 | Notes (adjustments from cross-evidence) |
|
||||
|---|---|---|
|
||||
| Architecture | 4 | Clean module separation, provider abstraction with a test seam, bounded background workers. −1 for runtime-prepared SQL throughout `db/repo.rs`. (evaluator) |
|
||||
| Security | **4** | Adjusted down from evaluator's 5. Core crypto/auth/SQL surfaces are genuinely 5-grade (constant-time compares, parameterized SQL, fail-closed webhook re-fetch). But a dedicated auditor surfaced **three independent P2s** (XFF bypass, dependency advisories, admin/public co-location) plus the exerciser's `file://` webhook-registration P2 — more hardening debt than a 5 carries until addressed. |
|
||||
| Performance | 4 | WAL + 8-conn pool + FK + busy_timeout; reconcile loop bounded. −1 for unbounded `rate_buckets` and the read-then-write limiter. (evaluator) |
|
||||
| Testing | 4 | ≈117–131 tests, 8 suites, all green; crosscheck verifies v1+v2 across all SDKs. No CI enforces it; harness has a portability bug (P1) and Rust examples don't build (P1). (evaluator + exerciser) |
|
||||
| Code quality | 4 | Comments explain *why*; idempotency where it matters. `cargo fmt` never run; a few very long handlers. (evaluator) |
|
||||
| Documentation | **4** | Adjusted down from evaluator's 5. Subsystem guides the evaluator read are accurate, but the doc-auditor's broader sweep found real drift: a published SDK README import that breaks compilation, a broken changelog link, stale Node/Rust versions, a contradicted merchant-profile entitlement status, and stale test counts. Solid but not pristine. |
|
||||
|
||||
## Disagreements & gaps
|
||||
|
||||
- **Severity disagreement, resolved:** evaluator rated the Zaprite webhook P2; security-auditor traced it end-to-end and rated it P0. Resolved to **P0** — it is unauthenticated, public, and mints signed licenses (direct funds loss for any operator running the optional Zaprite provider — which includes the master operator).
|
||||
- **Shared blind spot (all agents):** none could exercise the live BTCPay/Zaprite settlement→issuance path without real provider credentials. The evaluator and exerciser both labeled their payment-path confidence "medium." The single highest-value verification missing is a full purchase cycle against a real BTCPay/Zaprite sandbox — which also exposes the P0/P1 settle-path logic to a real test.
|
||||
- **Quality tension:** evaluator scored code-quality 5 on SQL discipline; the exerciser found input-validation gaps (slug) and framework error leaks. Both are true at different layers — the score holds, the caveat is noted.
|
||||
- **Security lens (5 vs 4):** the evaluator scored Security 5 from the crypto/auth/SQL code; the security-auditor (whose whole job is the adversarial pass) surfaced three P2 hygiene/hardening items. Resolved by adjusting to 4 with the core surfaces explicitly credited — no factual conflict, just depth of coverage.
|
||||
- **Documentation lens (5 vs 4):** the evaluator judged the subsystem guides it read (accurate → 5); the doc-auditor swept the SDK READMEs and public sites and found drift the evaluator didn't reach. Adjusted to 4.
|
||||
- **Shared blind spot — the live payment path:** every runtime-exercising agent stopped at the same wall. The exerciser could not test `POST /v1/purchase`, recover, upgrade, subscription-cancel, or BTCPay connect/disconnect (no live BTCPay/Zaprite); the security-auditor could not confirm whether the StartOS front proxy strips client-supplied XFF (which decides if the P2 rate-limit bypass is remotely reachable); the start9-spec-checker could not run on-box install/backup/restore. The single highest-value next test, named independently by multiple agents: run the `onboarding-harness` Stage 1+2 against a **regtest BTCPay** to exercise purchase → webhook-settle → license-issue end-to-end.
|
||||
- **No 0.4.x submission docs exist publicly:** the start9-spec-checker could only verify against the 0.3.5.x submission page (the only one Start9 links). Whether `prepare.sh` is still required for the 0.4.x SDK era is unconfirmable from public docs (the 0.4.x `hello-world` template also lacks one) — treated as a blocker pending Start9 confirmation.
|
||||
|
||||
## Suggested order of work
|
||||
|
||||
1. **Fix the Zaprite P0 with its test alongside.** On any settle event, re-fetch `get_invoice_status` from the provider and require `pending` local state + matching paid amount/currency before issuing — this closes both the P0 forgery and the P1 missing-amount-check in one change. (The reconciler already does the safe re-fetch; mirror it in the webhook handler.)
|
||||
2. **Add the provider-injection test seam first** (an always-compiled `Option<Arc<dyn PaymentProvider>>` override on `AppState`, checked in `resolve_provider_for_profile_rail`). This is a prerequisite for trusting any payment-path test result — it greens the two `paid_purchase_*` red tests AND gives step 1 a regression harness. Delete the dead `payment_provider_preference_round_trip` in the same pass.
|
||||
3. **Decide scoped API keys:** either finish the `require_scope` migration or stop advertising/issuing `ks_…` keys until it lands — a shipped-but-403 feature is worse than an absent one.
|
||||
4. **Quick-win hardening batch:** rate-limit `/v1/purchase` + `/v1/redeem`; JSON-ify 422/415; add slug validation; wire `GET /v1/admin/products`; bump `sqlx` ≥0.8.1 and `rustls`/`rustls-webpki`.
|
||||
5. **StartOS submission fixes** (before any registry submission): the two manifest dead links and the aarch64-vs-manifest decision are trivial; add `instructions.md`; confirm with Start9 whether the custom `LicenseRef-Keysat-1.0` satisfies their "source available" bar.
|
||||
6. **Add a CI gate** (Gitea Actions/Woodpecker running `cargo test` + `clippy`) so the honor-system test gap — the meta-cause behind the shipped `:52` and Zaprite defects — closes permanently.
|
||||
7. **Then resume feature work:** the product→merchant-profile picker (already scoped at the top of the AGENTS.md roadmap; the write path is the gating piece that makes multi-profile usable end-to-end) and the remaining deferred UIs.
|
||||
1. **Fix the two shipped P1 DX breakages first** (cheap, adopter-facing): correct every `licensing_client` → `keysat_licensing_client` import across the Rust SDK + `integrate.html`, and make `tests/crosscheck/run_ts.mjs` resolve the TS SDK build relative to its sibling dir instead of the hardcoded `/sessions/...` path. Add a crosscheck case feeding trailing bytes to all four SDKs (also closes the Go P3).
|
||||
2. **Patch the dependency advisories** (`sqlx ≥0.8.1`, pull `rustls-webpki ≥0.103.13` via the reqwest/rustls bump, update start-sdk for `fast-xml-parser`); re-run `cargo audit`/`npm audit`. Low-risk, mechanical, and removes the only externally-known CVEs.
|
||||
3. **Resolve the XFF rate-limit bypass** — first confirm whether the StartOS front proxy overwrites `X-Forwarded-For` (this decides reachability); then derive the client IP from the trusted-proxy connection / configured allowlist with a peer-socket fallback rather than raw XFF.
|
||||
4. **Stand up minimal CI** (one job: `cargo test && cargo clippy && tsc --noEmit`, ideally `cargo fmt --check`). This makes the already-good suite *trustworthy* and is a precondition for relying on the green status in everything below.
|
||||
5. **Retire the runtime-prepared-SQL risk class** on the money paths — migrate the purchase/settle/resolution queries in `db/repo.rs` + `subscriptions.rs` to compile-checked `sqlx::query!`, and add a `rate_buckets` reaper mirroring the session/redemption reapers.
|
||||
6. **Clear the doc drift in one pass** — resolve the `unlimited_merchant_profiles` contradiction with a live `GET /v1/products/keysat/policies` (then fix whichever of the guide/code-comment/AGENTS.md is wrong), and fix the ROADMAP/BUILDING/README/PORTING/testing-count/changelog-link items together.
|
||||
7. **Unblock the registry milestone** — author `prepare.sh` for the clean-Debian build, then do the on-box manual checklist (install → connect BTCPay → backup → uninstall → restore → verify key+licenses survive) and email submissions@start9.com about the source-available license before submitting.
|
||||
|
||||
Reference in New Issue
Block a user