All four SDKs are now published to their registries:
- npm: @keysat/licensing-client
- crates.io: keysat-licensing-client
- PyPI: keysat-licensing-client
- Go module proxy: github.com/keysat-xyz/keysat-client-go
Changes:
- §7a / §7b / §7c install blocks collapsed from "Install (preferred)
/ GitHub fallback" pairs to single registry-install lines. The
ssh-vs-https / prepare-script troubleshooting is no longer
relevant for the install path.
- New §7d: Go integration. Same shape as the other languages:
install snippet, embed-pubkey pattern, verify-on-startup,
use-at-feature-gate. Uses the Go SDK's IsTrial() method (not
manual flag math). hex.EncodeToString for the LicenseID byte
array.
- Existing §7d (Hard-gate patterns), §7e (Packaging gotchas),
§7f (Frontend integration) renumbered to §7e / §7f / §7g.
- Cross-references updated everywhere (§0, §6, §15).
- Header line updated: doc now claims Go support alongside the
existing three languages.
Critical bug fixes — code an LLM would copy verbatim:
- Wire format §4: clarify FLAG_FINGERPRINT_BOUND = bit 0 (mask 0x01),
FLAG_TRIAL = bit 1 (mask 0x02). The doc previously claimed
FLAG_TRIAL=1, which is wrong — that's the fingerprint-bound bit.
- Trial detection across §7a / §7b / §7c / §14: stop doing
`(flags & 1)` manually. TS/Python/Go SDKs pre-parse isTrial /
is_trial / IsTrial() on the payload. Rust requires manual math
but with the FLAG_TRIAL constant from the crate, not bit 0.
- Field name sweep: TS payload field is `licenseUuid` (or top-
level `licenseId` on the result root), not `payload.licenseId`.
Rust payload's `license_id` is `[u8; 16]` raw bytes — render to
hex for display. Updated examples to match each SDK's actual API.
- §9a cross-product safety: rewritten end-to-end. The payload
carries product UUID (not slug). The old doc told the LLM to
assert `payload.product_slug !== MY_SLUG`, which silently passes
because the field doesn't exist. New doc covers both correct
paths: online via `validate(slug, …)` (daemon resolves
slug→UUID), or offline by embedding the operator's product UUID.
Stale references / improvements:
- §0 Q4: cross-reference for hard-gate flavors corrected to §7d
(was pointing at §8 which is entitlement-naming). Added a
"soft-gate is the safe default" nudge.
- §0 new Q8: ask whether the operator already has an entitlements
catalog before drafting the config card.
- §7a GitHub fallback: trimmed. SDK repos are public and have
`prepare` scripts, so the ssh-vs-https troubleshooting saga
isn't needed anymore.
- §7d "Mode::Enforce" reference removed — that build-time flag
was deprecated. Keysat itself dogfoods soft-gate (always boots,
tier caps enforce at create-time).
New content for one-shot integration success:
- §8 / §11a: hidden_entitlements (v0.2.0:24) explained — buy page
filters them out; SDK consumers should too.
- §11a "Rendering tier cards": multi-currency formatter
(priceCurrency + priceValue), marketing_bullets +
marketing_bullets_position, featured_discount auto-apply via
the `code` option on startPurchase.
- §11a Common mistakes: assuming all prices are in sats; skipping
the featured-discount surfacing.
- New §15a "Verify your integration with curl": four-command
health-check the LLM can run before writing app code. Catches
slug typos, missing policies, unreachable daemon early.
- §15 Common mistakes: added the product UUID gotcha and the
flag bit-math gotcha as explicit entries.
LLM-consumer impact: the previous version had three subtle bugs
that survived offline signature verification — wrong trial
detection on every fingerprint-bound license, missing product
isolation across multi-product Keysats, and a wrong PRO-tier
default selection. All three failure modes are now flagged or
fixed in the doc; an LLM that follows the new doc literally
produces correct integration code.
Notes cover the entitlements catalog feature shipped in 68dfe7f
plus the four SDK 0.3.0 cuts (TS / Rust / Python / Go) that
surface the catalog on listPublicPolicies. Phase 2 (side-by-side
card-grid policy authoring UI) is queued for v0.2.0:9.
KEYSAT_INTEGRATION.md section 8 grows a subsection explaining the
catalog mechanics: bubble picker, buy page rendering, SDK surface,
catalog-stability rule.
Test count: 78 (unchanged from :7 except for migration_0014 already
counted in the prior commit).
Captures the offline-vs-online enforcement framing that every
operator hits when they realize they want to revoke / downgrade /
lapse a license. Previously this answer was scattered across
sections; consolidating into a dedicated section 0a so both LLMs
and humans following the integration doc see it before they make
the SDK call-pattern decision.
Covers:
- What the buyer's app can enforce offline (baked-in expiry,
entitlement set, trial flag, fingerprint binding)
- What the operator can change ONLY online (revocation, tier
changes, sub lapses, seat enforcement)
- The two design dials operators pick (baked-expiry length,
whether the app calls validate())
- The two patterns: A = "true perpetual, offline-only"; B =
"perpetual price, online-enforced entitlements"
- Side-by-side TS code samples for each pattern
- Operator-side implications for each product type (perpetual,
recurring, trial-converting)
- Cross-reference to section 11a (tier upgrades only have teeth
with Pattern B) so the LLM following that section's flow back
to here gets the right framing
- Note that Keysat itself dogfoods Pattern B (with reference
to the new license_self::refresh_self_tier_from_db helper)
The framing is the same one that came out of Grant's testing
session — the integration doc is now the canonical place to
point any future operator who asks "wait, why doesn't downgrading
take effect?"
Documents the multi-policy in-app purchase flow that the Recap dev
hit a dead-end on (no obvious tier discriminator on startPurchase).
Adds:
- New section 11a "Tier-aware purchases — in-app tier picker
(multi-tier products)" walking the full pattern: listPublicPolicies
→ render tier UI → startPurchase with policySlug → open checkout →
poll/webhook → write key. Same shape in TS / Python / Rust / Go.
- Architecture diagram showing buyer → SDK → daemon → BTCPay → key.
- "When you'd use this" guidance + "Common mistakes" section
including the four traps the Recap dev guessed at: hardcoding
slugs, splitting products, abusing discount codes as tier
selectors, omitting policySlug.
- Cross-reference from question 7 in section 0 (the operator-
questionnaire) so the LLM nudges toward the picker pattern when
there are 2+ tiers, and back to single-tier section 11 otherwise.
- Cross-reference from section 7f (frontend integration for
hard-gate Flavor 2) so the activation-screen pattern surfaces
the picker as an inline option.
- Cross-reference from section 11 → 11a so single-policy readers
who later add tiers find the upgrade path.
This is the pattern Recap implements in its activation screen, and
becomes the canonical example for any future multi-tier integration.
SDKs (TS, Rust, Python, Go) all support it as of their 0.2.0
releases (commits c3a57a0 / 5dd301c / 94654f6 / 970f95a in their
respective repos).
The previous commit removed the canonical 1378-line integration guide
based on a misread of intent — the file's "moved to startos folder"
note referred to *this* (licensing-service-startos) repo. The 12-line
stub at the parent licensing/ folder is the forwarder, not the canonical.
No version bump: doc-only restore, no on-disk or daemon behaviour
change. v0.1.0:41 release notes contain an incidental line stating
"KEYSAT_INTEGRATION.md is removed from this repo" — left as-is for
now since the .s9pk hasn't been re-published since :41. If we
re-publish :41 and the line bothers us, a separate commit can correct
it before the next .s9pk build.
The v0.1.0:40 migration was correct on clean installs but crashed at
COMMIT on any database with rows in discount_redemptions: SQLite's
deferred FK check saw the dropped parent's bookkeeping as unsatisfied
even after the rename. Fix is to rebuild discount_redemptions in the
same transaction (stash → drop → rebuild → restore) plus orphan
cleanup. Migration is idempotent; operators on :40 with a checksum
mismatch recover by deleting the version=9 row from _sqlx_migrations
and restarting.
Lands the missing migration test scaffolding too. The four tests in
licensing-service/tests/migrations.rs apply migrations against a
realistic populated database (products, policies, invoices, licenses,
machines, discount codes, redemptions, webhooks, tip attempts). The
regression test fails with the exact 787 error against the v40
migration — would have caught the bug pre-release.
KEYSAT_INTEGRATION.md is removed from this repo; it now lives in the
parent licensing/ folder.