Files
keysat/startos/versions/v0.1.0.ts
T
Grant 6ac118ae70 v0.1.0:24 — Keysat licensing service end-to-end
Daemon, StartOS wrapper, admin SPA, public buy/thank-you pages,
discount codes, free-license redemption, Apply-discount UX,
self-licensing, and v0.1.0 release notes.
2026-05-07 10:33:39 -05:00

141 lines
15 KiB
TypeScript

// Current version of the package. Migrations get added here as versions
// increment.
//
// Version-string format is ExVer: `<upstream>:<downstream>`. Downstream
// revision is bumped for wrapper-only or daemon-only changes that don't
// alter on-disk data shape (we use SQLite migrations for schema changes
// rather than ExVer-level migrations).
import { VersionInfo } from '@start9labs/start-sdk'
export const v0_1_0 = VersionInfo.of({
version: '0.1.0:24',
releaseNotes: [
`Alpha-iteration revision 24 of v0.1.0 — Apply-discount button on the buy page + delete discount codes from the admin UI.`,
``,
`Buy page (/buy/<slug>) — buyers can now click an "Apply" button next to the discount code input to preview the discount before committing. The price card updates with strikethrough on the original price, the new price, and a green tag showing the percent or sats off. If the code is a free_license type, the primary CTA flips from "Pay with Bitcoin" to "Redeem license" and skips the BTCPay path entirely on submit. Validation happens against a new public endpoint GET /v1/discount-codes/preview which checks existence/active/expiry/product/exhaustion and computes the discounted price WITHOUT consuming a redemption slot. Editing the code after Apply resets the price card.`,
``,
`Admin UI — discount codes table now has a Delete button next to Disable/Enable. Hard-deletes the code with a confirmation prompt. Backed by a new endpoint DELETE /v1/admin/discount-codes/:id that refuses with 409 Conflict if any redemptions reference the code (preserves the audit trail). Operators should keep using Disable for redeemed codes; Delete is for cleaning up codes that were created but never used.`,
``,
`New public endpoint: GET /v1/discount-codes/preview?code=…&product=… — used by the buy page Apply button. Returns {valid, code, kind, is_free, base_price_sats, discount_applied_sats, final_price_sats, amount_pct, message}. Same pricing math as /v1/purchase, kept in sync.`,
``,
`New admin endpoint: DELETE /v1/admin/discount-codes/:id — audit-logged as discount_code.delete; returns 409 Conflict with a clear message if the code has been redeemed.`,
``,
`Net effect: the buy page is now a single-form purchase flow that handles paid + free + discount-coded purchases without surprises, and the admin can prune mistakenly-created codes.`,
``,
`No DB schema changes since :23.`,
``,
`Alpha-iteration revision 23 of v0.1.0 — buyers actually receive their license after paying.`,
``,
`Three coordinated fixes:`,
`1. KEYSAT_PUBLIC_URL is now picked using pickPublicUrl (clearnet preferred) instead of pickBrowserUrl (mDNS preferred). The daemon's own public URL needs to resolve from random buyer browsers, not just the operator's LAN.`,
`2. purchase.rs now defaults BTCPay's redirect_url to {public_base_url}/thank-you?invoice_id=<internal-id> so BTCPay sends the buyer back to a Keysat page after payment. Internal invoice id is also used as the local row id (was previously a fresh UUID), so /v1/purchase/<internal_id> and /thank-you?invoice_id=<id> both resolve to the same row.`,
`3. /thank-you completely rewritten as a buyer-facing license-display page. Reads ?invoice_id from the URL, polls /v1/purchase/<id> every 3 seconds, renders the license in a certificate-style card with a Copy button when issued. Polls for up to 12 minutes before giving up. Falls back to a friendly error if the invoice id is missing/invalid.`,
``,
`Net effect: after paying via BTCPay, buyers land on a Keysat-branded thank-you page that auto-displays their license key as soon as the BTCPay webhook fires and the daemon issues the license. No StartOS dashboard required — this is a pure end-buyer flow.`,
``,
`Database change: repo::create_invoice now takes the invoice id as a parameter (was previously self-generated). Backwards-compatible at the schema level.`,
``,
`Alpha-iteration revision 22 of v0.1.0 — buy page auto-handles free-license discount codes.`,
``,
`Before: pasting a discount code of kind 'free_license' on /buy/<slug> still tried to create a BTCPay invoice for the post-discount sat amount, which BTCPay rejected with "amount below dust threshold" for tiny amounts. Buyers had to manually curl /v1/redeem to actually use free codes.`,
``,
`Now: when a code is provided, the buy page tries POST /v1/redeem first. If the code is free_license type, the daemon issues a license directly with no payment leg and the page renders the license key inline in a certificate-style success card with a Copy button. If the code is percent or fixed_sats type, /v1/redeem returns "this code requires payment" and the page falls through to the standard BTCPay purchase flow with the code applied. Real code errors (unknown, expired, wrong product) surface to the buyer cleanly.`,
``,
`Net effect: free-license codes now Just Work via the normal buyer UI. Useful for press, beta testers, partners, the early-100-users plan, etc.`,
``,
`Alpha-iteration revision 21 of v0.1.0 — actually fix the buyer-facing checkout URL.`,
``,
`Bug found via :20 diagnostic logs: BtcpayProvider::create_invoice (the trait method) had the rewrite logic, but purchase.rs uses the compat shim state.btcpay_client() which returns the raw BtcpayClient and bypasses the trait entirely. Result: the rewrite was never reached, and buyers always got the internal Docker hostname.`,
``,
`Fix: apply the same rewrite_to_public helper inline in purchase.rs after BtcpayClient::create_invoice returns. Same diagnostic log lines now fire from the purchase code path. Eventually purchase.rs (and reconcile.rs, tipping.rs) will migrate fully to the PaymentProvider trait — that's a v0.3 cleanup. For now the rewrite happens in both places so the urgent buyer-facing bug is fixed.`,
``,
`Operator action: install, then make a fresh purchase. The new log line "purchase: checkout URL rewritten for buyer" with original/rewritten URLs should appear, and the Pay-with-Bitcoin redirect should land on \`https://btcpay.<your-domain>/i/...\`.`,
``,
`Alpha-iteration revision 20 of v0.1.0 — diagnostic logging on the BTCPay checkout-URL rewrite path.`,
``,
`On startup the daemon now logs the resolved \`btcpay_url\`, \`btcpay_browser_url\`, and \`btcpay_public_url\` so it's clear what the wrapper handed in. On every checkout-URL rewrite, BtcpayProvider logs the original URL, the rewritten URL, and the public_base used. If public_base is None (no rewrite), it logs a loud warning explaining what to check.`,
``,
`Use these logs to diagnose any remaining "buyer gets the internal .startos URL" issue: tail Keysat logs, kick a purchase, look for "checkout URL rewritten" (good) or "checkout URL NOT rewritten" (misconfig — wrapper or env var problem).`,
``,
`No code-flow changes since :19; pure observability bump.`,
``,
`Alpha-iteration revision 19 of v0.1.0 — buyer-facing checkout URLs now use clearnet domain instead of mDNS.`,
``,
`:18 added a checkout-URL host rewrite, but used the same URL-picker as the operator OAuth redirect — which prefers mDNS/LAN URLs (good for the operator on the same LAN as the Start9, useless for buyers on the public internet). The rewrite produced URLs like \`https://immense-voyage.local:49347/i/...\` that random buyers couldn't resolve.`,
``,
`:19 splits the pickers. New \`pickPublicUrl\` prefers domain-named clearnet URLs (e.g. \`https://btcpay.your-domain.com\`) over IP/mDNS, used specifically for buyer-facing checkout URL rewrites. \`pickBrowserUrl\` (operator OAuth flow) keeps preferring LAN/mDNS — operator is local, faster path. New env var \`BTCPAY_PUBLIC_URL\` plumbs the public-preferred URL into the daemon, and the BtcpayProvider's host-rewrite uses it instead of \`BTCPAY_BROWSER_URL\`.`,
``,
`Operator action: install, then Disconnect → Connect BTCPay one more time to refresh the active provider with the new public URL. After that, /buy/<slug> should produce checkout URLs at your clearnet BTCPay domain (e.g. \`https://btcpay.keysat.xyz/i/...\`) which buyers can actually open.`,
``,
`Falls back to the old behaviour (BTCPAY_BROWSER_URL = mDNS) only if no clearnet URL is configured for BTCPay — useful for local-only testing but won't produce working URLs for real customers.`,
``,
`Alpha-iteration revision 18 of v0.1.0 — proper fix for BTCPay checkout URLs (revert :17, rewrite at the boundary instead).`,
``,
`:17 changed BTCPAY_URL to BTCPay's public StartTunnel URL for API calls. That broke the OAuth Connect flow because the Keysat container can't reliably reach the public URL from outside (StartOS egress routing). Reverted that.`,
``,
`Better fix: keep API calls on the internal \`btcpayserver.startos:23000\` hostname (fast, always reachable). Then in the BtcpayProvider's create_invoice path, rewrite the checkout URL's host (scheme + host + port) from the internal one to BTCPay's public URL before returning to the buyer. Path/query/fragment are preserved. Buyers now get a working public URL; daemon-to-daemon API calls stay internal.`,
``,
`Operator action: install this version, then run Disconnect BTCPay → Connect BTCPay once to refresh stored connection state. After that, /buy/<slug> purchases should produce a checkout URL like \`https://btcpay.<your-domain>/i/...\` instead of \`btcpayserver.startos:23000/i/...\`.`,
``,
`New cargo dep: \`url = "2"\` (already transitively present via reqwest; now declared directly for the host-rewrite helper).`,
``,
`Alpha-iteration revision 17 of v0.1.0 — fix BTCPay URL handed to daemon (checkout URLs were broken for buyers).`,
``,
`Bug: BTCPAY_URL was hard-coded to the internal Docker hostname \`btcpayserver.startos:23000\`. When Keysat created an invoice via BTCPay's API at that URL, BTCPay generated a checkout URL using the same internal hostname — and any buyer hitting that checkout URL got a "Server Not Found" error because \`.startos\` only resolves on the local Start9.`,
``,
`Fix: BTCPAY_URL now defaults to BTCPay's PUBLIC URL (the same URL used for browser redirects during the authorize flow). API calls cost a small out-and-back through StartTunnel per invoice — invoice creation is rare and the URL correctness wins. Falls back to the internal URL if the public URL hasn't been enumerated yet.`,
``,
`After installing this version, run Disconnect BTCPay → Connect BTCPay once to refresh the stored connection state, then test with a fresh /buy/<slug> purchase. The checkout URL should now be \`https://btcpay.<your-domain>/i/...\` instead of \`btcpayserver.startos:23000/i/...\`.`,
``,
`Alpha-iteration revision 16 of v0.1.0 — public buyer-facing purchase page.`,
``,
`New route: GET /buy/:slug. Server-renders a Keysat-branded HTML page for a given product slug — name, description, price-in-sats, optional email + discount code form, "Pay with Bitcoin" button. The button POSTs via JS to /v1/purchase, gets the BTCPay checkout URL, redirects the buyer there. After payment BTCPay returns them to /thank-you (existing handler).`,
``,
`Inlined navy/cream/gold styling matches the rest of the Keysat brand. Self-contained — no asset hosting required. 404 for inactive or missing slugs with a friendly explanation page.`,
``,
`Operator's "buy URL to share with customers" is now: https://<keysat-host>/buy/<product-slug>. Update marketing copy / install docs to point at this URL.`,
``,
`Alpha-iteration revision 15 of v0.1.0 — admin-only issuer-key import endpoint for master-Keysat bootstrap.`,
``,
`New endpoint: POST /v1/admin/import-issuer-key. Accepts a PEM-encoded Ed25519 private key in the request body, validates it, and upserts into the server_keys table replacing the auto-generated keypair. Refuses if any licenses have already been issued (safety guard against accidentally invalidating customer keys). Audit-logged. Restart the service after a successful import for the new keypair to take effect.`,
``,
`Why this isn't a StartOS Action: it'd clutter every operator's UI to serve a one-time setup for the single master operator. Documented in MASTER_KEYPAIR_PROCEDURE.md as the canonical bootstrap path. Curl during master-Keysat setup, never touched by the 95% of operators selling their own software.`,
``,
`No DB schema changes. No new dependencies.`,
``,
`Alpha-iteration revision 14 of v0.1.0 — Marketplace icon updated to the new Keysat brand mark (gold key on a navy-bordered certificate). Cosmetic only — no code or schema changes since :13.`,
``,
`Alpha-iteration revision 13 of v0.1.0 — PaymentProvider abstraction (Phase 1 of multi-provider work).`,
``,
`Refactor only — no user-visible behavior change. Sets up v0.3 to add Zaprite as a second payment provider alongside BTCPay without parallel code paths.`,
``,
`New module 'src/payment/' defines a PaymentProvider trait with create_invoice / get_invoice_status / validate_webhook / pay_lightning_invoice methods. BtcpayProvider is the first impl, wrapping the existing BtcpayClient and HMAC webhook secret. The webhook handler now dispatches through the trait — same BTCPay flow, but the abstraction is exercised end-to-end so we know the design holds before Zaprite arrives.`,
``,
`AppState replaces its 'btcpay' field with 'payment: Arc<RwLock<Option<Arc<dyn PaymentProvider>>>>'. Existing BTCPay-specific call sites (purchase, reconcile, tipping) unchanged; they go through compat accessors that downcast the trait object back to BtcpayProvider. Those compat accessors retire in v0.3 as the call sites migrate.`,
``,
`New cargo dep: async-trait (for object-safe async methods on the new trait).`,
``,
`No DB schema changes vs :12.`,
``,
`Earlier in the v0.1.0 line:`,
`:12 — Tip-recipient on policy + Support development footer link.`,
`:11 — Keysat-licenses-Keysat dogfooded; daemon embeds master pubkey, verifies /data/keysat-license.txt at boot; new "Activate Keysat license" + "Show license status" StartOS actions.`,
`:10 — admin web UI restyled in Keysat brand (navy/cream/gold).`,
`:9 — admin web UI made functional; Actions trimmed to setup-only.`,
`:8 — embedded admin SPA scaffolding (placeholder).`,
`:7 — operator-name live-reload; idempotent Connect BTCPay; Disconnect action; payment-method check.`,
`:6 — CSRF state encoded inside redirect URL.`,
`:5 — URL ranking applied to our own public URL.`,
`:4 — URL ranking by browser-reachability for BTCPay's URL.`,
`:3 — getAll() over BTCPay interfaces, filter by type='ui'.`,
`:2 — broader BTCPay URL filter for LAN-only setups.`,
`:1 — kebab-case action IDs; task severity 'important'; root in container; BTCPAY_BROWSER_URL plumbing.`,
`:0 — initial release.`,
].join('\n'),
migrations: {
up: async () => {},
down: async () => {},
},
})