Files
keysat-root/AGENTS.md
T

178 lines
11 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.
# AGENTS.md — Keysat workspace
Self-hosted, Bitcoin-native software licensing service running as a StartOS 0.4.x
package, with four wire-compatible SDKs and a public landing/docs site.
This file holds whole-repo, every-session facts. Subsystem detail lives in scoped
guides under `docs/guides/` (symlinked to `.claude/rules/` so Claude Code
auto-loads each when you edit matching files). **Before editing a subsystem, read
its guide** — see the index below.
## Stack
- **Daemon**: Rust 1.88, `axum`, `sqlx` + SQLite, Ed25519 signing.
- **Wrapper**: TypeScript, `@start9labs/start-sdk ^1.3.2`, `@vercel/ncc` bundle, Node 22.
- **SDKs**: TS (npm), Rust (crates.io), Python (PyPI), Go (proxy.golang.org).
- **Platform**: StartOS 0.4.0.x (LXC under the hood — commands/paths reflect that, not Docker).
- **Payment providers**: BTCPay Server (required dep); Zaprite (optional, gated by `zaprite_payments`).
## Subsystem guides (read before editing the area)
- Before editing the daemon source, read `docs/guides/daemon-architecture.md`.
- Before editing payment / provider / merchant-profile code or migrations 00200022, read `docs/guides/payments.md`.
- Before touching self-license or tier-gating code, read `docs/guides/licensing-tiers.md`.
- Before changing the LIC1 wire format, crypto, or crosscheck fixtures, read `docs/guides/crypto-wire-format.md`.
- Before building, bumping the version, or editing the StartOS wrapper, read `docs/guides/startos-packaging.md`.
- Before editing the admin SPA (`web/index.html`), read `docs/guides/admin-ui.md`.
- Before editing public site/docs copy, read `docs/guides/website-copy.md`.
- Before adding/altering tests or relying on lint/CI, read `docs/guides/testing.md`.
## Build / test / run (quick ref)
From `licensing-service-startos/`: `make x86` | `make arm` | `make universal` |
`make install` | `make clean` | `npm run check`. From
`licensing-service-startos/licensing-service/`: `cargo check` | `cargo build
--release` | `cargo test` | `cargo test --test <suite>` | `cargo test <name>`.
Details, the version-bump-before-build rule, and release scripts:
`docs/guides/startos-packaging.md`. Test suites, the no-CI / formatting-not-enforced
status, and known-failing tests: `docs/guides/testing.md`.
## Directory layout
```
licensing-service-startos/ daemon + StartOS wrapper (s9pk package source)
licensing-service/src/ Rust daemon → guides/daemon-architecture.md
licensing-service/migrations/ SQLite migrations (numbered, additive)
licensing-service/web/index.html embedded admin SPA → guides/admin-ui.md
licensing-service/tests/ integration suites → guides/testing.md
startos/ wrapper TS → guides/startos-packaging.md
Dockerfile Makefile s9pk.mk build pipeline
keysat-xyz-landing/ keysat-docs/ keysat-registry-landing/ public sites → guides/website-copy.md
licensing-client-{rust,ts,python,go}/ the four SDK source repos
activate-license-template/ Tauri desktop template for license activation
keysat-design-system/ design tokens / brand assets
plans/ design specs (multi-provider-payment-model.md, keysat-smtp-emails.md)
tests/crosscheck/ cross-language LIC1 verifier → guides/crypto-wire-format.md
```
Note: the daemon (`licensing-service-startos`, repo `keysat`), each SDK, and
`plans/` are **separate git repos** — commit code/plan changes in their own repo.
The root `Licensing` repo (`keysat-root`) tracks only `AGENTS.md` + `docs/guides/`
+ `.claude/rules/` + `EVALUATION.md` (the latest full-eval report; overwritten
each run, history in git log). **Remotes differ per repo**: the daemon's `main` tracks
**GitHub** (`origin`, the public upstream) with a `gitea` backup — plain `git push`
goes to GitHub, so also `git push gitea main`; root + plans are **Gitea-only**.
Run `git remote -v` (full) and check what the branch tracks before pushing.
## Conventions (whole-repo)
- Daemon licensed `LicenseRef-Keysat-1.0` (custom, source-available); SDKs MIT.
- Commits in imperative mood, body only when the "why" isn't obvious. **Sign as
Keysat (Grant), not Claude** — git user is `Keysat`.
- Direct push to `main` + run `~/.keysat/publish.sh` is the authorized release flow
until launch.
- Never rewrite user-facing copy outside the explicit scope of a request.
## Never
- **No AI co-authorship** on commits or PRs (no "Co-Authored-By", no "Generated with…").
- **Don't push `--no-verify`** or bypass hooks unless explicitly authorized.
- **Don't commit built artifacts** (`*.s9pk`, `keysat-*.s9pk`, `javascript/`) or
**secrets** — reference env-var names; real values live in `~/.keysat/filebrowser.env`
and `/data/keysat-license.txt` outside the repo.
## Memory references
Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-licensing-Licensing/memory/`
(scan before a major change): `keysat_release_workflow.md`,
`no_unauthorized_copy_changes.md`, `keysat_admin_ui_pill_convention.md`,
`startos_lxc.md`, `startos_registry_icon_unrenderable.md`, `keysat_open_threads.md`.
## Open TODOs
- Extend `publish.sh` to build + upload aarch64 (arm builds fine; only x86 ships
today), or narrow the manifest's arch claim. `riscv` target unverified.
- StartOS Community Registry submission criteria — Start9 hasn't published the
checklist; reach out directly when ready.
- Registry icon doesn't render in the StartOS marketplace (see `guides/startos-packaging.md`).
## Current state (2026-06-13)
- **Live**: server `immense-voyage.local` runs daemon `0.2.0:54` (migrations
00200022 applied). Registry `registry.keysat.xyz` publishes `:54` too
(GitHub release `v0.2.0-54` cut; `files.keysat.xyz` serves the s9pk). Four SDKs
published; `keysat.xyz` + `docs.keysat.xyz` deployed.
- **`:52`/`:53` = multi-provider/merchant-profile model**: data model + backend
resolution shipped and audited sound; the resolution/CRUD query surface now has
test coverage. See `docs/guides/payments.md`.
- **Two payment-path fixes shipped 2026-06-13**: (a) `:53` fixed the `:52`
ambiguous-column bug that broke *every* paid purchase (daemon `31f4670`); (b)
`:54` fixed the **P0 Zaprite webhook-forgery** — settle now re-confirms against
the provider API before issuing (daemon `783372c`, bump `495fbbf`). Both built,
installed to prod, and published to the registry. Live purchase + settle paths
are sound.
- **GAP — multi-profile is non-functional end-to-end**: nothing in the shipped
app writes `products.merchant_profile_id` (the INSERT in
`create_product_with_currency` omits it; `update_product_with_currency` has no
field for it; the `Product` struct in `models.rs` doesn't even carry it). So
every product created post-migration stays on the default profile, and a Pro
operator can create extra profiles + attach providers but cannot route any
product's sales to them. The data model + resolver fully support it; only the
product→profile **write path** is missing. **This is the gating piece for
multi-profile** — see the scoped slice below.
- **Triage from `EVALUATION.md` (full-eval, 2026-06-13)** — P0/P1 = work queue,
P2 = known debt, P3+ = deferred. The report at repo root has file:line evidence;
it's tracked, so re-running full-eval overwrites it and `git log -- EVALUATION.md`
preserves prior runs.
- **Work queue (P0/P1 — do first, in this order)**:
1.**SHIPPED in `:54` — Provider-injection test seam.** Added the
always-compiled `AppState::provider_override` + `provider_from_row` helper at
every resolution site; greened the two `paid_purchase_*` tests; deleted the
dead `payment_provider_preference_round_trip`. See `docs/guides/testing.md`.
2.**SHIPPED in `:54` — Zaprite webhook forgery fix.** `webhook.rs::handle_inner`
re-fetches `provider.get_invoice_status` and requires `Settled` before any
settle-derived action; acks 200 (no issue) when the provider is unreachable.
Two regression tests (forged-settle, provider-unreachable). api 47/47.
**Still open (auditor P1):** a literal paid-amount/currency check — needs a
trait change (`get_invoice_status` returns only a status enum). See
`docs/guides/payments.md`. **This is now the top remaining security item.**
3. **[P1] Scoped API keys (`ks_…`) are non-functional** — issuable but 403 on
every admin endpoint; the `require_admin``require_scope` migration was never
done. Finish it, or stop advertising/issuing them. `api/api_keys.rs:14`.
- **Then resume feature work**: the **product→merchant-profile picker** (the GAP
above — slice: add `merchant_profile_id` to the `Product` model + `repo.rs`
SELECT mapping; a `set_product_merchant_profile` follow-up writer mirroring
`set_product_entitlements_catalog`; the field on `CreateProductReq`/
`UpdateProductReq` applied post-write; a profile `<select>` from
`GET /v1/admin/merchant-profiles` in the create+edit product forms, shown only
when >1 profile; no migration), the 3 other deferred UIs (rail picker,
per-profile SMTP, rail-pref editor), and `unlimited_merchant_profiles` on
master Pro/Patron policies.
- **Known debt (P2 — schedule, not urgent)**: no rate-limit on `/v1/purchase` +
`/v1/redeem`; rate-limit bucket keys on spoofable `X-Forwarded-For` (bypass
conditional on whether the StartOS proxy rewrites XFF — unverified); `422`/`415`
errors return plain-text not JSON (breaks SDK `JSON.parse`); product `slug` has
no validation (empty/300-char/meta chars stored); `GET /v1/admin/products`
returns 405 though OpenAPI documents it; dep advisories (`sqlx`→≥0.8.1
RUSTSEC-2024-0363, `rustls-webpki`→≥0.103.12); **4 StartOS submission blockers**
missing `instructions.md`, dead `packageRepo` (`…/keysat-startos``…/keysat`) +
`docsUrls` (`/docs/``/licensing-service/docs/`) manifest links, aarch64
declared-but-not-shipped; no CI + fmt/clippy/prettier unenforced.
- **Deferred (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 self-heal `_sqlx_migrations` allowlist foot-gun; 2 KB unauth Zaprite
payload WARN-log; outbound-webhook SSRF (operator-only); stale
`versions/v0.2.0.ts:3-4` "NOT YET WIRED" comment; 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); api
43 pass / 3 known-fail (now tracked in the work queue above), other suites
green. FK enforcement **confirmed** — sqlx pool sets `foreign_keys(true)` per
connection (`db/mod.rs`). CI/fmt status is in Known debt.