v0.2.0:11 + v0.2.0:12 — Archive, Settings, agent surface, machines redesign

Two release cycles prepared together: v0.2.0:11 (policy archive + safe-
delete cleanup + brand-consistent confirm modals) and v0.2.0:12 (Settings
tab + agent-friendly operator API + machines tab redesign + buyer-facing
copy alignment).

Highlights:

- Migration 0015: policies.archived_at column. Archive button on tier
  cards; safe-delete relaxed to ignore revoked-license tombstones;
  renewal worker refuses archived policies.
- Migration 0016: scoped_api_keys table. Four roles (read-only,
  license-issuer, support, full-admin) with bounded scopes. Master
  admin_api_key still works on every endpoint; scoped keys gated on
  endpoints wired through require_scope().
- New /v1/openapi.json — public, no auth. Curated OpenAPI 3.1 spec
  for agent / SDK discovery.
- New Settings tab: Operator name + Payment providers panel + API
  keys management. Replaces 8 StartOS Actions (Zaprite all, BTCPay
  all, operator name, switch-provider). StartOS Actions pruned to 4
  install-time essentials.
- Machines tab rewritten: global default view grouped by product,
  filter pills with counts, quick-stats row, drill-down via new
  "Machines" button on each Licenses-tab row. New repo helper
  list_machines_admin joins machines x licenses x products
  server-side.
- Branded confirmModal replaces every native window.confirm() call
  in the admin UI (7 callsites).
- Enforce mode killed: KEYSAT_LICENSE_ENFORCE compile-time flag
  retired; daemon always boots; missing self-license -> Creator
  (free) tier. "Unlicensed" label gone from admin UI.
- Zaprite gated on the new zaprite_payments entitlement (renamed
  from card_payments to reflect the broader gateway).
- Creator code cap 5 -> 10.
- KEYSAT_AGENT_GUIDE.md: auth, role-to-scope mapping, error envelope,
  webhook events, worked recipes.
- Buyer-facing copy aligned with new positioning: "Bitcoin-native
  self-hosted software licensing" everywhere on production surfaces.
- Cross-product safety section (Section 9a) added to KEYSAT_INTEGRATION.md.
- 5 new API integration smoke tests covering OpenAPI, scoped API
  keys CRUD, role-elevation guard, and Zaprite-tier gating.

Test count: 83 passing (was 78). All migration tests pass against
0015 and 0016 applied to populated DBs.
This commit is contained in:
Grant
2026-05-11 08:45:25 -05:00
parent 20b5293c81
commit 257669092b
25 changed files with 2980 additions and 384 deletions
+23 -42
View File
@@ -1,56 +1,37 @@
// Register actions with StartOS.
//
// As of v0.1.0:11 the StartOS Actions tab is intentionally minimal —
// only setup-time operations live here:
// The StartOS Actions tab is kept intentionally minimal — only the
// four operations that need to happen outside the admin web UI:
//
// - General → Set operator name
// - BTCPay → Connect / Check / Disconnect
// - License → Activate Keysat license / Show license status
// - Credentials → Show admin API key
// - Set web UI password — needed for password recovery (you can't
// reset the password from inside the web UI if you can't log in)
// - Activate Keysat license — first-install bootstrap for paid
// customers, and recovery if /data/keysat-license.txt gets lost
// - Show license status — sanity-check the self-license state
// without logging into the admin UI
// - Show credentials — find the admin API key on first install,
// before you've logged into the admin UI for the first time
//
// Everything else (products, policies, discount codes, licenses,
// machines, webhooks, audit log) lives in the embedded admin web UI
// at /admin/. The action source files remain in this directory for
// reference — and the underlying admin HTTP API is unchanged — but
// they're no longer registered as StartOS UI buttons. This keeps the
// dashboard from feeling like an undifferentiated wall of buttons.
//
// The web UI uses the same /v1/admin/* endpoints those actions used to
// call, so functionality is identical; only the UI surface changed.
// Everything else — operator name, payment provider connect / activate,
// scoped API keys, products, policies, licenses, codes, machines,
// webhooks, audit log — lives in the embedded admin web UI under the
// Settings tab and the workspace sidebar. The action source files for
// those operations remain in this directory for reference, but they're
// no longer registered as StartOS UI buttons. This keeps the dashboard
// from feeling like an undifferentiated wall of buttons and aligns with
// "everything in one place" — the web UI.
import { sdk } from '../sdk'
import { activateLicense, showLicenseStatus } from './activateLicense'
import { switchPaymentProvider } from './activatePaymentProvider'
import { btcpayStatus, configureBtcpay, disconnectBtcpay } from './configureBtcpay'
import {
configureZaprite,
disconnectZaprite,
showZapriteWebhookSetup,
zapriteStatus,
} from './configureZaprite'
import { setOperatorName } from './setOperatorName'
import { setWebUiPassword } from './setWebUiPassword'
import { showCredentials } from './showCredentials'
export const actions = sdk.Actions.of()
// General
.addAction(setOperatorName)
// First-install / recovery essentials.
.addAction(setWebUiPassword)
// BTCPay setup (Bitcoin-only payments via your own BTCPay Server)
.addAction(configureBtcpay)
.addAction(btcpayStatus)
.addAction(disconnectBtcpay)
// Zaprite setup (Bitcoin + fiat-card payments via Zaprite's broker)
.addAction(configureZaprite)
.addAction(zapriteStatus)
.addAction(showZapriteWebhookSetup)
.addAction(disconnectZaprite)
// Single unified switch action — flips active provider via a
// dropdown so operators don't see two confusing "Activate X"
// actions side-by-side, each appearing to override the other.
.addAction(switchPaymentProvider)
// Keysat self-license (Keysat-licenses-Keysat)
.addAction(showCredentials)
// Keysat self-license (Keysat-licenses-Keysat). Required for paid
// customers to activate their self-license on first install. The
// license string itself is provided by your seller.
.addAction(activateLicense)
.addAction(showLicenseStatus)
// Credentials
.addAction(showCredentials)
+56 -1
View File
@@ -58,6 +58,61 @@ const RELEASE_NOTES = [
// in RELEASE_NOTES above (the milestone). Subsequent revisions
// append here.
const ROUTINE_NOTES = [
'0.2.0:12 — **Settings tab + agent-friendly operator API.** Major release that consolidates operator configuration into the admin UI and ships first-class agent / AI-automation support: OpenAPI 3.1 spec, scoped API keys, agent integration guide. Plus a slate of UX cleanups carried over from operator testing.',
'',
'**New Settings tab in the admin UI.** Three subsections in one place:',
'',
'- **Operator name** — the display name shown on /buy/<slug> + thank-you pages. Was in StartOS Actions; now lives where the rest of operator configuration lives.',
'- **Payment providers** — BTCPay + Zaprite connect / disconnect / activate, with provider status and active-provider toggle in one view. Replaces 5 separate StartOS Actions. Zaprite is grayed out + tagged "locked" for operators on Creator tier; clicking through opens the Pro upgrade page (gated on the `zaprite_payments` entitlement).',
'- **API keys** — generate / list / revoke scoped Bearer tokens for agents and automation.',
'',
'**Scoped API keys (migration 0016).** Master `KEYSAT_ADMIN_API_KEY` is full-access and reserved for the operator. Scoped keys are additional credentials with bounded permissions. Pick a role at generate time:',
'',
'- `read-only` — list everything, mutate nothing',
'- `license-issuer` — reads + issue / revoke / suspend / change-tier on licenses; cannot touch products, policies, or codes',
'- `support` — license-issuer + cancel subscriptions + deactivate machines',
'- `full-admin` — every scope except operator-only settings (payment providers, operator name, generating other API keys)',
'',
'Tokens look like `ks_<43 chars>`, are returned ONCE on create (never again — only sha256 stored), and can be revoked instantly from the same UI. Endpoints that have been wired through `require_scope` accept either the master key or a scoped key with the appropriate role. Endpoints not yet wired stay master-only — secure by default.',
'',
'**OpenAPI 3.1 spec.** `GET /v1/openapi.json` returns a curated, stable spec covering the agent-relevant subset of endpoints. No auth required. Drop the URL into a Custom GPT, OpenAI Assistant, LangChain, Claude Code, or any agent framework with OpenAPI support and the agent discovers Keysat\'s API automatically.',
'',
'**Agent integration guide.** New `KEYSAT_AGENT_GUIDE.md` doc covering scoped key generation, role-to-scope mapping, error envelope conventions (every error response returns `{ok: false, error: <stable_code>, message: ...}` with stable codes like `tier_cap`, `not_found`, `license_revoked`), webhook event types, and worked recipes for common automation patterns.',
'',
'**Landing page — agent-friendly section.** New section at keysat.xyz/#agents calling out OpenAPI discovery, scoped API keys, and HMAC-signed webhooks, with a link to the integration guide. Top-nav gains "Agents" anchor.',
'',
'**Enforce mode killed.** The compile-time `KEYSAT_LICENSE_ENFORCE=1` flag is gone. Every Keysat binary now boots permissively — a missing or invalid self-license falls back to the Creator (free) tier instead of refusing to start. Simpler mental model: every Keysat install is Creator-tier out of the box, paid tiers add entitlements. The "Unlicensed" label is gone from the admin UI; the free state surfaces as "Creator" everywhere.',
'',
'**Zaprite gated to Pro (`zaprite_payments` entitlement).** Both Connect Zaprite and Activate Zaprite now check the daemon\'s self-tier and return 402 with an upgrade URL when the entitlement is missing. BTCPay (Bitcoin / Lightning) remains free on every tier. Renamed the entitlement from `card_payments` to `zaprite_payments` to reflect that the Zaprite gateway covers cards plus Apple Pay, bank transfers, and more — not just credit cards.',
'',
'**StartOS Actions reduced to 4 essentials.** Pruned 8 actions that duplicated admin-UI functionality. What remains: `Set web UI password`, `Activate Keysat license`, `Show license status`, `Show credentials`. Everything else (operator name, payment providers, products, policies, licenses, codes, machines, webhooks, audit log, scoped API keys) lives in the admin UI under Settings or the workspace sidebar.',
'',
'**Creator code cap bumped 5 → 10.** Promo campaigns multiply faster than products do; doubling the active-codes cap removes a friction point that wasn\'t actually driving upgrades. Existing operators see the new cap immediately.',
'',
'**Machines tab — global default view + drill-down.** The old "paste a license ID to see anything" form is gone. Default view now lists every machine across every license, grouped by product, with the same product / status filter pills used by Licenses and Subscriptions. Quick-stats row shows total machines + active count + top platform. Each row shows buyer email, license id (with status pill), hostname, platform, last IP, last heartbeat as a relative date. The Licenses tab now has a "Machines" button on every license row that drills directly into that license\'s machines without copy-pasting UUIDs. New repo helper `list_machines_admin` joins machines × licenses × products server-side so there\'s no N+1 fetch. `GET /v1/admin/machines` is backwards-compatible — the old `?license_id=X` form still works (it\'s how the drill-down passes through).',
'',
'**Buyer-facing copy aligned with the new positioning.** Thank-you page + buy page footers, registry tagline, OpenAPI spec description, every SDK README, and the integration guide all now say "Bitcoin-native self-hosted software licensing." The older "Bitcoin-paid" phrasing is fully retired from production-facing surfaces.',
'',
'**UX polish.** Welcome card removed from Overview (was redundant). Analytics opt-in card aligned with other Overview cards and copy tightened. support.html stripped down to Patron / Lightning / OpenSats (the speculative "what funds go toward" section is gone). Thank-you page status-detail copy honest about Lightning vs on-chain settle timing. Reason-modal label has proper vertical spacing. Stray "legacy" references in user-facing copy swept.',
'',
'**Cross-product safety doc.** New §9a in KEYSAT_INTEGRATION.md explaining the same-keypair-multiple-products situation: licenses for any product on an operator\'s instance signature-verify against the same key, so applications must assert `payload.product_slug` after offline verify OR pass `product_slug` to online validate. Daemon already enforces it when product_slug is passed; the SDK doesn\'t auto-assert offline. New bullet in §15 Common mistakes points at §9a.',
'',
'**Test count: 78 + new tests landed in this release** (full suite passes across the 8 categories: unit / api / migrations / btcpay / crosscheck / etc.). The api.rs test for `payment_provider_preference_round_trip` was updated to pin the test daemon to a licensed Pro tier so Zaprite activation still passes through the new gate.',
'',
'**Upgrade path.** v0.2.0:11 → v0.2.0:12 is a drop-in. Migration 0016 is additive (one new table). No SDK changes. **One operator-facing behavior change**: operators currently on a Pro/Patron self-license should rename `card_payments` → `zaprite_payments` in their `keysat` product\'s entitlements catalog + on Pro/Patron policy rows. Until they do, Connect/Activate Zaprite will 402 even though they bought the right tier. (Master operator: run this rename on your master instance, then re-mint and re-issue customer self-licenses.) Pre-launch this affects very few people.',
'',
'0.2.0:11 — **Policy archive + saner delete semantics + brand-consistent confirm modals.** Three closely-related cleanups driven by hands-on testing: policies can now be archived (soft-hide) instead of forced through hard-delete, the safe-delete check stopped treating revoked-license tombstones as blockers, and every confirm dialog in the admin UI now uses the in-app overlay card instead of the browser-native one.',
'',
'**Archive a policy.** New Archive button on every tier card. Archived policies are hidden from the admin grid (toggle "Show archived" at the top of the Policies tab to reveal them), hidden from `/buy/<slug>`, and the renewal worker refuses to renew subscriptions tied to an archived policy (dispatches a `subscription.renewal_skipped` webhook with `reason: policy_archived`). Existing licenses keep validating because entitlements are signed into the LIC1 payload — the policy row is not consulted at validate time. Reversible: Unarchive button on archived cards puts everything back. Migration 0015 adds `policies.archived_at TEXT NULL`; existing rows default to NULL (live).',
'',
'**Safe-delete ignores tombstones.** Previously, any reference at all — including revoked-license rows kept for audit — blocked the safe-delete path and pushed the operator to force-delete. Now the safe-delete check counts only **live licenses** (status != revoked), **settled invoices**, and **active subscriptions** (status in active/past_due). Revoke an outstanding license and the policy is immediately safe-deletable, with the cascade sweeping up the revoked tombstone + any dead invoices. Force-delete still works the same way (wipes everything regardless), and the cascade now also handles `tier_changes` rows + nulls out `discount_codes.applies_to_policy_id` so it doesn\'t fall over on FK violations.',
'',
'**Branded confirm dialogs.** Every `window.confirm()` call in the admin UI replaced by a new `confirmModal()` overlay-card helper styled the same as the existing `reasonModal()`. No more "immense-voyage.local:62488" host string at the top of irreversible-action confirmations. Sites swept: policy delete, product delete, analytics-ID wipe, admin tier downgrade, discount-code delete, machine force-deactivate, webhook subscription delete. Modal copy adapts to context — e.g. deleting a policy with zero licenses now just asks "Sure?" without spelling out the cascade implications.',
'',
'**Test count: 78** (no test changes; framework PR coming separately).',
'',
'**Upgrade path.** v0.2.0:10 → v0.2.0:11 is a drop-in. Migration 0015 is additive (one nullable column + an index). No SDK changes. No behavior change for operators who don\'t use the new Archive button — existing policies stay live until someone explicitly archives them.',
'',
'0.2.0:10 — **Licenses + Subscriptions tabs reorganized to match Products + Policies.** Both tabs now group by product (matching the per-product card sections used elsewhere in the admin UI), with product-filter pills + per-product license counts at the top. Single-product instances continue to see a flat table; multi-product instances see one section per product with a status breakdown subtitle ("3 active · 1 revoked · 2 expired"). Search results bypass grouping (search is global across all products).',
'',
'**Licenses tab gains a quick-stats row** matching the Overview dashboard: Licenses, Active, Revoked, Expiring within 30 days. Scope follows the active product filter — pick a product, the stats reflect just that product. Hover the "?" icons next to each stat label for definitions.',
@@ -209,7 +264,7 @@ const ROUTINE_NOTES = [
].join('\n\n')
export const v0_2_0 = VersionInfo.of({
version: '0.2.0:10',
version: '0.2.0:12',
releaseNotes: { en_US: ROUTINE_NOTES },
// No on-disk transformation needed — v0.2.0:0 is a label change.
// SQLite-level migrations live separately under