KEYSAT_INTEGRATION.md: collapse install paths to registries + add Go section
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.
This commit is contained in:
+138
-53
@@ -1,9 +1,9 @@
|
||||
# Integrating Keysat licensing into your software
|
||||
|
||||
This document is the complete instruction set for adding Keysat-based
|
||||
licensing to any application. It covers Node/TypeScript, Python, and Rust.
|
||||
Hand it to an LLM (or a developer) along with your codebase and ask them
|
||||
to wire it up — they should have everything they need.
|
||||
licensing to any application. It covers Node/TypeScript, Python, Rust,
|
||||
and Go. Hand it to an LLM (or a developer) along with your codebase and
|
||||
ask them to wire it up — they should have everything they need.
|
||||
|
||||
## How to use this document
|
||||
|
||||
@@ -51,7 +51,7 @@ hangs on these:
|
||||
but won't function without a paid license. The binary is essentially
|
||||
a locked installer until the buyer activates. Common for closed-source
|
||||
paid apps and for open-source apps that the operator chooses to
|
||||
monetize through the registry distribution. See section **7d** for the
|
||||
monetize through the registry distribution. See section **7e** for the
|
||||
two flavors of hard gating (refuse-to-start vs. activate-screen-only).
|
||||
- **Nag mode** — no enforcement; just a "support development" banner
|
||||
when unlicensed. Pure honor system. Useful when the app is
|
||||
@@ -416,7 +416,10 @@ StartOS Actions UI for buyers to paste keys into.
|
||||
Every integration follows the same shape regardless of language and
|
||||
regardless of which enforcement model from question 4 the operator picked.
|
||||
The verify-once-at-startup primitive is the same; what you do with the
|
||||
result is what changes.
|
||||
result is what changes. The doc is structured the same way: section 7
|
||||
covers the verify primitive in each language; section 7e covers the
|
||||
hard-gate enforcement flavors; the worked examples in section 14 show
|
||||
soft-gate; the patterns are mix-and-match.
|
||||
|
||||
```
|
||||
on startup:
|
||||
@@ -437,11 +440,11 @@ on startup:
|
||||
# Then — depending on the operator's chosen model:
|
||||
#
|
||||
# HARD GATE : if not licensed, exit (Flavor 1) or block all
|
||||
# business endpoints (Flavor 2). See section 7d.
|
||||
# business endpoints (Flavor 2). See section 7e.
|
||||
#
|
||||
# SOFT GATE : run normally; specific feature handlers consult
|
||||
# license_state.entitlements before unlocking.
|
||||
# See section 7a/7b/7c.
|
||||
# See section 7a/7b/7c/7d.
|
||||
#
|
||||
# NAG MODE : run normally; show a "support development" banner
|
||||
# in the UI when license_state.state != 'licensed'.
|
||||
@@ -449,7 +452,7 @@ on startup:
|
||||
|
||||
The verify-and-populate-state step is identical for all three models.
|
||||
The doc is structured the same way: section 7 covers the verify
|
||||
primitive in each language; section 7d covers the hard-gate enforcement
|
||||
primitive in each language; section 7e covers the hard-gate enforcement
|
||||
flavors; the worked examples in section 14 show soft-gate; the
|
||||
patterns are mix-and-match.
|
||||
|
||||
@@ -483,27 +486,12 @@ direct callers but the timer keeps humming along.
|
||||
|
||||
### 7a. TypeScript / Node
|
||||
|
||||
**Install (preferred, once published):**
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
npm install @keysat/licensing-client
|
||||
```
|
||||
|
||||
**GitHub fallback** (the npm package is pending publication; the GitHub
|
||||
repo is public and installable directly):
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
"dependencies": {
|
||||
"@keysat/licensing-client": "git+https://github.com/keysat-xyz/keysat-client-ts.git"
|
||||
}
|
||||
```
|
||||
|
||||
Use the explicit `git+https://` form (not the `github:user/repo` shorthand),
|
||||
which avoids the ssh-vs-https resolution drift that bites hermetic build
|
||||
environments. The SDK's `prepare` script builds `dist/` automatically on
|
||||
git install, so no extra steps are needed.
|
||||
|
||||
**Embed the public key.** The simplest way is to commit the PEM file
|
||||
to your repo at `assets/issuer.pub` and import it as a raw string:
|
||||
|
||||
@@ -592,23 +580,12 @@ app.post('/api/export', (req, res) => {
|
||||
|
||||
### 7b. Python
|
||||
|
||||
**Install (preferred, once published):**
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
pip install keysat-licensing-client
|
||||
```
|
||||
|
||||
**GitHub fallback** (if the PyPI package isn't published yet). The
|
||||
`keysat-xyz/keysat-client-python` repo must be **public** on GitHub
|
||||
for this to work in clean environments:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/keysat-xyz/keysat-client-python.git
|
||||
```
|
||||
|
||||
(Python's pip-from-git path is simpler than npm's — no separate build
|
||||
step is required since pure-Python packages are installable from source.)
|
||||
|
||||
**Embed the public key** at a path your code can read:
|
||||
|
||||
```python
|
||||
@@ -694,23 +671,14 @@ def export_endpoint():
|
||||
|
||||
### 7c. Rust
|
||||
|
||||
**Install (preferred, once published):**
|
||||
**Install:**
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dependencies]
|
||||
keysat-licensing-client = "0.1"
|
||||
keysat-licensing-client = "0.3"
|
||||
```
|
||||
|
||||
**Git fallback** (if not on crates.io yet). The
|
||||
`keysat-xyz/keysat-client-rust` repo must be **public** on GitHub:
|
||||
|
||||
```toml
|
||||
keysat-licensing-client = { git = "https://github.com/keysat-xyz/keysat-client-rust.git" }
|
||||
```
|
||||
|
||||
Cargo builds from source, so no separate build step is required.
|
||||
|
||||
**Embed the public key:**
|
||||
|
||||
```rust
|
||||
@@ -815,7 +783,124 @@ if !lic.entitlements.contains("export") {
|
||||
}
|
||||
```
|
||||
|
||||
### 7d. Hard-gate patterns — "the app doesn't function without a license"
|
||||
### 7d. Go
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
go get github.com/keysat-xyz/keysat-client-go@latest
|
||||
```
|
||||
|
||||
The package is `github.com/keysat-xyz/keysat-client-go` (imported as
|
||||
`keysat`). Stdlib-only — no third-party Go dependencies.
|
||||
|
||||
**Embed the public key:**
|
||||
|
||||
```go
|
||||
import _ "embed"
|
||||
|
||||
//go:embed assets/issuer.pub
|
||||
var issuerPEM string
|
||||
```
|
||||
|
||||
**Verify on startup:**
|
||||
|
||||
```go
|
||||
// internal/license/license.go
|
||||
package license
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/keysat-xyz/keysat-client-go"
|
||||
)
|
||||
|
||||
const ProductSlug = "<your-product-slug>"
|
||||
|
||||
//go:embed ../../assets/issuer.pub
|
||||
var issuerPEM string
|
||||
|
||||
var issuerKey ed25519.PublicKey
|
||||
|
||||
func init() {
|
||||
k, err := keysat.LoadPublicKeyPEM(issuerPEM)
|
||||
if err != nil {
|
||||
panic("bad embedded issuer pubkey: " + err.Error())
|
||||
}
|
||||
issuerKey = k
|
||||
}
|
||||
|
||||
type State struct {
|
||||
State string // "licensed" | "unlicensed" | "invalid"
|
||||
Reason string
|
||||
LicenseID string
|
||||
Entitlements map[string]struct{}
|
||||
ExpiresAt *time.Time
|
||||
IsTrial bool
|
||||
}
|
||||
|
||||
func readKey() string {
|
||||
if v := strings.TrimSpace(os.Getenv("MYAPP_LICENSE_KEY")); v != "" {
|
||||
return v
|
||||
}
|
||||
path := os.Getenv("MYAPP_LICENSE_KEY_PATH")
|
||||
if path == "" {
|
||||
path = "/data/license.txt"
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
func Check() State {
|
||||
raw := readKey()
|
||||
if raw == "" {
|
||||
return State{State: "unlicensed"}
|
||||
}
|
||||
payload, err := keysat.ParseAndVerify(raw, issuerKey)
|
||||
if err != nil {
|
||||
return State{State: "invalid", Reason: err.Error()}
|
||||
}
|
||||
ents := make(map[string]struct{}, len(payload.Entitlements))
|
||||
for _, e := range payload.Entitlements {
|
||||
ents[e] = struct{}{}
|
||||
}
|
||||
var exp *time.Time
|
||||
if payload.ExpiresAt != 0 {
|
||||
t := time.Unix(payload.ExpiresAt, 0).UTC()
|
||||
exp = &t
|
||||
}
|
||||
return State{
|
||||
State: "licensed",
|
||||
LicenseID: hex.EncodeToString(payload.LicenseID[:]),
|
||||
Entitlements: ents,
|
||||
ExpiresAt: exp,
|
||||
IsTrial: payload.IsTrial(), // method, not field — Go SDK pre-parses
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use it:**
|
||||
|
||||
```go
|
||||
lic := license.Check()
|
||||
log.Printf("[license] state=%s entitlements=%v", lic.State, lic.Entitlements)
|
||||
|
||||
// At a feature gate:
|
||||
if _, ok := lic.Entitlements["export"]; !ok {
|
||||
http.Error(w, `{"error":"feature_not_in_tier","message":"Export requires a paid license."}`, http.StatusPaymentRequired)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 7e. Hard-gate patterns — "the app doesn't function without a license"
|
||||
|
||||
If the operator chose **hard gate** in the section-0 questions (binary
|
||||
freely downloadable, but locked until activated), use one of these two
|
||||
@@ -967,7 +1052,7 @@ favor of always-permissive boot + tier-cap enforcement at create-time.
|
||||
The pattern is a good reference for soft-gate or hard-gate-Flavor-2 in
|
||||
your own app: never block boot; gate work on entitlements.
|
||||
|
||||
### 7e. Packaging gotchas — Docker, s9pk, hermetic builds
|
||||
### 7f. Packaging gotchas — Docker, s9pk, hermetic builds
|
||||
|
||||
Most non-trivial integrations end up packaged in Docker (Start9 s9pk,
|
||||
generic container deploys, CI-built images). The following gotchas
|
||||
@@ -1043,11 +1128,11 @@ rm myapp_x86_64.s9pk && make x86
|
||||
**5. The `--ignore-scripts` flag will skip the SDK's `prepare` build.**
|
||||
If your Dockerfile uses `npm ci --ignore-scripts` (a common security
|
||||
hardening), the SDK won't build its `dist/` and you'll hit the
|
||||
"Cannot find module" runtime error from §7a. Either drop
|
||||
"Cannot find module" runtime error from the npm install. Either drop
|
||||
`--ignore-scripts` for the builder stage, or pre-build the SDK
|
||||
elsewhere and vendor `dist/` in.
|
||||
|
||||
### 7f. Frontend integration for hard-gate Flavor 2
|
||||
### 7g. Frontend integration for hard-gate Flavor 2
|
||||
|
||||
If you picked hard-gate Flavor 2 (server starts, business endpoints
|
||||
return 402 until activated), **the frontend is half the work** —
|
||||
@@ -1864,7 +1949,7 @@ ship it.
|
||||
If your Dockerfile lists individual server files explicitly, adding
|
||||
`server/license.js` requires its own `COPY` line. Build succeeds,
|
||||
container starts, then crashes at startup with `Cannot find module
|
||||
'./license.js'`. See §7e for the full Docker checklist.
|
||||
'./license.js'`. See §7f for the full Docker checklist.
|
||||
- **Letting the SDK ship without a built `dist/`.** Git installs of
|
||||
the Keysat client *only* work if the package has a `prepare` script
|
||||
that builds on install (or commits its `dist/` directory). Without
|
||||
@@ -1881,7 +1966,7 @@ ship it.
|
||||
- **Skipping the frontend half of hard-gate Flavor 2.** A server-only
|
||||
integration boots happily but every request 402s, which the
|
||||
unlicensed user experiences as a broken app rather than a clear
|
||||
"activate to continue" screen. See §7f for the framework-agnostic
|
||||
"activate to continue" screen. See §7g for the framework-agnostic
|
||||
pattern.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user