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
|
# Integrating Keysat licensing into your software
|
||||||
|
|
||||||
This document is the complete instruction set for adding Keysat-based
|
This document is the complete instruction set for adding Keysat-based
|
||||||
licensing to any application. It covers Node/TypeScript, Python, and Rust.
|
licensing to any application. It covers Node/TypeScript, Python, Rust,
|
||||||
Hand it to an LLM (or a developer) along with your codebase and ask them
|
and Go. Hand it to an LLM (or a developer) along with your codebase and
|
||||||
to wire it up — they should have everything they need.
|
ask them to wire it up — they should have everything they need.
|
||||||
|
|
||||||
## How to use this document
|
## How to use this document
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ hangs on these:
|
|||||||
but won't function without a paid license. The binary is essentially
|
but won't function without a paid license. The binary is essentially
|
||||||
a locked installer until the buyer activates. Common for closed-source
|
a locked installer until the buyer activates. Common for closed-source
|
||||||
paid apps and for open-source apps that the operator chooses to
|
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).
|
two flavors of hard gating (refuse-to-start vs. activate-screen-only).
|
||||||
- **Nag mode** — no enforcement; just a "support development" banner
|
- **Nag mode** — no enforcement; just a "support development" banner
|
||||||
when unlicensed. Pure honor system. Useful when the app is
|
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
|
Every integration follows the same shape regardless of language and
|
||||||
regardless of which enforcement model from question 4 the operator picked.
|
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
|
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:
|
on startup:
|
||||||
@@ -437,11 +440,11 @@ on startup:
|
|||||||
# Then — depending on the operator's chosen model:
|
# Then — depending on the operator's chosen model:
|
||||||
#
|
#
|
||||||
# HARD GATE : if not licensed, exit (Flavor 1) or block all
|
# 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
|
# SOFT GATE : run normally; specific feature handlers consult
|
||||||
# license_state.entitlements before unlocking.
|
# 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
|
# NAG MODE : run normally; show a "support development" banner
|
||||||
# in the UI when license_state.state != 'licensed'.
|
# 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 verify-and-populate-state step is identical for all three models.
|
||||||
The doc is structured the same way: section 7 covers the verify
|
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
|
flavors; the worked examples in section 14 show soft-gate; the
|
||||||
patterns are mix-and-match.
|
patterns are mix-and-match.
|
||||||
|
|
||||||
@@ -483,27 +486,12 @@ direct callers but the timer keeps humming along.
|
|||||||
|
|
||||||
### 7a. TypeScript / Node
|
### 7a. TypeScript / Node
|
||||||
|
|
||||||
**Install (preferred, once published):**
|
**Install:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @keysat/licensing-client
|
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
|
**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:
|
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
|
### 7b. Python
|
||||||
|
|
||||||
**Install (preferred, once published):**
|
**Install:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install keysat-licensing-client
|
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:
|
**Embed the public key** at a path your code can read:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -694,23 +671,14 @@ def export_endpoint():
|
|||||||
|
|
||||||
### 7c. Rust
|
### 7c. Rust
|
||||||
|
|
||||||
**Install (preferred, once published):**
|
**Install:**
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Cargo.toml
|
# Cargo.toml
|
||||||
[dependencies]
|
[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:**
|
**Embed the public key:**
|
||||||
|
|
||||||
```rust
|
```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
|
If the operator chose **hard gate** in the section-0 questions (binary
|
||||||
freely downloadable, but locked until activated), use one of these two
|
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
|
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.
|
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,
|
Most non-trivial integrations end up packaged in Docker (Start9 s9pk,
|
||||||
generic container deploys, CI-built images). The following gotchas
|
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.**
|
**5. The `--ignore-scripts` flag will skip the SDK's `prepare` build.**
|
||||||
If your Dockerfile uses `npm ci --ignore-scripts` (a common security
|
If your Dockerfile uses `npm ci --ignore-scripts` (a common security
|
||||||
hardening), the SDK won't build its `dist/` and you'll hit the
|
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
|
`--ignore-scripts` for the builder stage, or pre-build the SDK
|
||||||
elsewhere and vendor `dist/` in.
|
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
|
If you picked hard-gate Flavor 2 (server starts, business endpoints
|
||||||
return 402 until activated), **the frontend is half the work** —
|
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
|
If your Dockerfile lists individual server files explicitly, adding
|
||||||
`server/license.js` requires its own `COPY` line. Build succeeds,
|
`server/license.js` requires its own `COPY` line. Build succeeds,
|
||||||
container starts, then crashes at startup with `Cannot find module
|
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
|
- **Letting the SDK ship without a built `dist/`.** Git installs of
|
||||||
the Keysat client *only* work if the package has a `prepare` script
|
the Keysat client *only* work if the package has a `prepare` script
|
||||||
that builds on install (or commits its `dist/` directory). Without
|
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
|
- **Skipping the frontend half of hard-gate Flavor 2.** A server-only
|
||||||
integration boots happily but every request 402s, which the
|
integration boots happily but every request 402s, which the
|
||||||
unlicensed user experiences as a broken app rather than a clear
|
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.
|
pattern.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user