Keylight 0.4.1
dotnet add package Keylight --version 0.4.1
NuGet\Install-Package Keylight -Version 0.4.1
<PackageReference Include="Keylight" Version="0.4.1" />
<PackageVersion Include="Keylight" Version="0.4.1" />
<PackageReference Include="Keylight" />
paket add Keylight --version 0.4.1
#r "nuget: Keylight, 0.4.1"
#:package Keylight@0.4.1
#addin nuget:?package=Keylight&version=0.4.1
#tool nuget:?package=Keylight&version=0.4.1
Keylight C# SDK
Open-source C# SDK for Keylight — license your .NET, Godot, and Unity apps with online activation and offline Ed25519 license verification.
In one line: a software-licensing SDK for C# — license-key activation and validation, entitlement/feature gating, trials, and tamper-resistant offline license verification (signed
v3lease, Ed25519 + clock-skew tolerance) for .NET apps, Godot 4 (via NuGet), and Unity (UPM package coming). Async-native, dependency-light, and fully nullable-annotated.
Why Keylight
Licensing shouldn't mean bolting a heavyweight, phone-home-or-die SDK onto your app.
- Works offline. The license is a signed lease your app verifies locally with Ed25519 — no network round-trip to gate a feature, no lockout when the machine is offline.
- Tamper-resistant by design. Entitlements live inside the signature; a forged or hand-edited lease can't pass verification without the tenant's private key.
- Async-native, no surprises. Every network operation is a
Task-returning async method withCancellationTokensupport.StateandHasEntitlementare synchronous reads from the in-memory cache — no deadlocks. - One SDK family. Verifies licenses identically to the Swift, Rust, and JavaScript SDKs, proven by shared conformance vectors.
Table of Contents
- Why Keylight
- Features
- Runtime Support
- Quick Start
- License Lifecycle
- License States
- Entitlements
- Offline Validation
- Refresh and Trials
- Configuration Reference
- Godot and Unity
- Conformance
- Documentation
- Other SDKs
- License
Features
- License Lifecycle — Activate, validate, and deactivate license keys with a small, explicit API.
- Offline Verification — The single offline artifact is a signed
v3lease, verified with Ed25519 and a 300-second clock-skew tolerance. An optionalMaxOfflineDaysgrace caps how long a device may run without checking in. - Async-native — All network operations are
Task-returning withCancellationTokensupport. Synchronous wrappers (Activate,Validate,Deactivate) are provided for non-async contexts. - Synchronous reads —
StateandHasEntitlementare synchronous, backed by the in-memory lease cache — no deadlocks from sync-over-async. - Entitlements — Feature gating from the cached lease:
client.HasEntitlement("pro"). - Trials — Built-in local trial timer, auto-started on first launch via
CheckOnLaunchAsync. - Device Telemetry — Auto-attaches
sdk_version,platform, and (optional)app_versionon every API call. - Network Resilience — Validate network failures are non-fatal (the client retains whatever state the cached lease dictates).
- Pluggable — Swap the storage backend (
ILeaseStore) or HTTP transport (IKeylightTransport) via interfaces for tests or custom platforms. - Nullable-annotated — Targets
netstandard2.0andnet8.0; ships with#nullable enablethroughout and an.snupkgsymbol package.
Runtime Support
Targets netstandard2.0 (broad compatibility: .NET Framework 4.6.1+, .NET Core 2.0+, Mono,
Godot 4 via NuGet) and net8.0. The core library has zero runtime dependencies — pure
managed code with no NuGet references (important for IL2CPP/WebGL targets).
A Unity UPM package (dev.keylight.sdk) is available — see Godot and Unity.
Quick Start
dotnet add package Keylight
using System.Net.Http;
using Keylight;
// Fetch the tenant's trusted Ed25519 keyset so leases can be verified offline.
// (You can also pin keys explicitly via .TrustedKeys(...) on the builder.)
var http = new HttpClient();
var keyset = await Keyset.FetchAsync(http, "https://api.keylight.dev", "your-tenant");
var config = KeylightConfig
.Builder("your-tenant", "your-product", "sdk_live_…")
.TrustedKeys(keyset?.Keys ?? new Dictionary<string, string>())
.MaxOfflineDays(15) // optional offline grace window (15 is the default)
.Build();
var client = new KeylightClient(config);
// Activate a license key (online). The returned lease is Ed25519-verified
// *before* anything is persisted.
await client.ActivateAsync("USER-LICENSE-KEY");
// Gate features on entitlements — synchronous, from the cached lease.
if (client.HasEntitlement("pro"))
{
// unlock pro features
}
// Release the seat when uninstalling / switching devices.
await client.DeactivateAsync();
Call
await client.CheckOnLaunchAsync()on startup to refresh the lease if it is stale, and to auto-start the trial clock on first launch whenTrialDurationDaysis configured.
License Lifecycle
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ActivateAsync │────▶│ ValidateAsync │────▶│ DeactivateAsync │
└─────────────────┘ └──────────────────┘ └──────────────────┘
▲
│ on launch / on events (no background timers)
┌──────────────────────────┐
│ RefreshIfNeededAsync │
└──────────────────────────┘
| Method | Description |
|---|---|
ActivateAsync(key) |
Activates a key on this device. Verifies the returned lease before persisting; throws ActivationException on server error and LeaseVerificationFailedException on a bad signature. |
ValidateAsync() |
Re-checks the stored license online. Updates the cache if the server returns a new lease; network failures are non-fatal. |
DeactivateAsync() |
Releases the seat and clears local license state, even if the network call fails. Call on uninstall or device switch. |
RefreshIfNeededAsync() |
Validates only if due (debounce 5 min, stale 6 h, or within 24 h of expiry). Safe to call often. |
CheckOnLaunchAsync() |
Convenience: refresh if a license is stored; also auto-starts the trial clock on first launch. |
ActiveRevalidateAsync() |
Forces a validate on active use (foreground / popover / focus), debounced 60 s in memory. Bypasses the staleness gates so a revoke lands mid-session instead of at the next launch. Never throws; a transient failure never downgrades a live session. |
RefreshAfterUpgradeAsync(timeout?, pollInterval?) |
Polls validate after a purchase or plan change until the entitlements or State differ from when the call started. Returns true on a change (a revoke counts), false on timeout, cancellation, or when no license is stored (nothing is sent). Defaults 30 s / 2 s; the interval is floored at 100 ms. Never throws. |
FetchConfigAsync() |
Explicitly refreshes the server-owned product settings from /config. Never throws; a failure keeps the last known settings. CheckOnLaunchAsync already does this for unlicensed installs. |
Synchronous wrappers Activate(key), Validate(), and Deactivate() are provided for callers
that cannot use async/await (every await in the async path uses ConfigureAwait(false)).
License States
client.State resolves a single high-level status from the cached, signature-verified lease
(no network call). It is a KeylightState enum:
| State | Meaning |
|---|---|
Licensed |
Current, signature-valid active lease. |
Trial |
No license, but a local trial is active. |
Expired |
Lease expired, or a previously stored license is no longer current. Also mapped from lease status: "fallback" (cross-SDK note: Swift and Rust surface a distinct Limited state; C# maps it to Expired). |
Invalid |
No valid lease and no trial in progress. |
switch (client.State)
{
case KeylightState.Licensed:
// full access
break;
case KeylightState.Trial:
// trial UI
break;
case KeylightState.Expired:
case KeylightState.Invalid:
// prompt to activate
break;
}
Entitlements
Entitlements are feature keys carried inside the signed lease and checked offline:
if (client.HasEntitlement("cloud-sync"))
{
EnableCloudSync();
}
HasEntitlement returns true only when the cached lease is signature-valid, unexpired, and not
expired-status — so offline feature gating never disagrees with the resolved Expired state.
When MaxOfflineDays is set, it also gates on the offline grace window.
Offline Validation
The offline artifact is a signed v3 lease issued by the Keylight API. The SDK reconstructs
the exact signed payload (entitlements sorted, pipe-delimited) and verifies it with Ed25519
against the tenant's trusted keyset, applying a 300-second clock-skew tolerance.
// Pin trusted keys explicitly instead of fetching them:
var config = KeylightConfig
.Builder("your-tenant", "your-product", "sdk_live_…")
.TrustedKeys(new Dictionary<string, string>
{
{ "k1", "<raw Ed25519 public key, base64>" }
})
.MaxOfflineDays(15) // default; omit to run offline as long as the lease itself is current
.Build();
- The trusted keyset can be fetched once with
Keyset.FetchAsync(http, baseUrl, tenantId)or pinned at build time via.TrustedKeys(...). client.Stateandclient.HasEntitlementread from the in-memory verified-lease cache — no network call.
Refresh and Trials
There are no background timers. The host drives refresh on launch and on meaningful events:
await client.CheckOnLaunchAsync(); // validate if due + auto-start trial clock
await client.RefreshIfNeededAsync(); // call again on window-focus / purchase / resume
await client.ActiveRevalidateAsync(); // app came forward: force a check (60 s debounce)
RefreshIfNeededAsync is the cheap, often-called path — it skips the server when the cache is
fresh. ActiveRevalidateAsync is the prompt one: it always talks to the server (debounced to
60 s) so a dashboard revoke takes effect within minutes of the user touching the app rather than
waiting for the lease to expire or the app to relaunch. Wire it to whatever "the user is here
now" signal your host has — app activation, window focus, menu-bar popover opening.
Trials are local and offline-first. Set TrialDurationDays on the builder as a seed, then call
CheckOnLaunchAsync — the trial clock is started automatically on the first launch when no trusted
active license is present:
var config = KeylightConfig
.Builder("your-tenant", "your-product", "sdk_live_…")
.TrialDurationDays(14)
.Build();
var client = new KeylightClient(config);
await client.CheckOnLaunchAsync(); // starts the trial on first launch
if (client.State == KeylightState.Trial)
{
ShowTrialBanner();
}
Server-owned settings
The trial length and the free-tier flag are settings the server owns; you change them in the
dashboard, not in a release. They ride on every validate response and on /config, which
CheckOnLaunchAsync fetches for installs that have no license to validate. The value on the
builder is only a seed for an install that has never reached the server.
client.EffectiveTrialDurationDays(); // server value → TrialDurationDays seed → 0
client.EffectiveFreeTierEnabled(); // server value → false (there is no seed)
await client.FetchConfigAsync(); // refresh explicitly; failures keep the last known values
EffectiveFreeTierEnabled only reports the flag — KeylightState has no free-tier member, so what a
free tier unlocks is your call. To verify these settings against the keys you compile in, see
.RequireSignedConfig(bool) below.
After a purchase
When the user buys or changes plan in a browser, the app only finds out by asking. Rather than write the polling loop yourself:
if (await client.RefreshAfterUpgradeAsync()) // 30 s, polling every 2 s
{
UnlockPaidFeatures(); // entitlements or State changed
}
It snapshots the entitlements and State when called, then validates every pollInterval until
either differs, returning true as soon as that happens. A transient failure is swallowed and
polling continues; a timeout, a cancelled token, or an install with no stored license returns
false (the last of those sends nothing).
Configuration Reference
Built with KeylightConfig.Builder(tenantId, productId, sdkKey):
| Builder method | Type | Default | Description |
|---|---|---|---|
(required) Builder(tenantId, productId, sdkKey) |
string |
— | Your Keylight tenant, product, and SDK key. All three are required. |
.TrustedKeys(dict) |
IDictionary<string,string> |
empty | Trusted Ed25519 public keys (kid → base64) for offline verification. |
.MaxOfflineDays(n) |
int |
15 |
Offline grace window since last online validation. Set 0 to run offline as long as the lease itself is current. |
.TrialDurationDays(n) |
int |
— | Seed trial length in days, used until the server's value arrives. Omit to disable trials on a fresh install. |
.RequireSignedConfig(bool) |
bool |
false |
Reject server-owned settings that do not carry a valid Ed25519 signature from TrustedKeys. Leave off unless your product is signed (the worker signs only products with a trial length configured); rejected settings fall back to the seed, never to what the server claimed. |
.AppVersion(v) |
string |
— | Reported in activation/validation telemetry. |
.KeyPrefix(p) |
string |
— | Client-side key-format check (e.g. "PROD"). |
.BaseUrl(url) |
string |
https://api.keylight.dev |
API base URL. |
Godot and Unity
Godot 4 — install via the standard NuGet workflow. Add Keylight to your project's
.csproj or use the Godot editor's NuGet integration. The netstandard2.0 target is compatible
with Godot's .NET 6+ Mono runtime.
Unity — a UPM (Unity Package Manager) package (dev.keylight.sdk) is included in this repo
under unity/dev.keylight.sdk. Install it via Unity's Package Manager (UPM) using the Git URL, or
via OpenUPM once published. The package source is kept in sync with
src/Keylight via unity/sync-core.sh.
Conformance
The security-critical lease verifier is gated by Keylight's frozen cross-SDK conformance vectors
(tests/Keylight.Tests/ConformanceTests.cs). The C# verifier must agree with every vector on
{ KidKnown, SignatureValid, Expired }, which keeps offline verification behavior identical across
the Keylight SDK family (Swift, Rust, JavaScript, C#, …).
dotnet test tests/Keylight.Tests
Documentation
- Platform docs: docs.keylight.dev
- Website: keylight.dev
- API host:
https://api.keylight.dev
Other SDKs
| Platform | Status | Repository |
|---|---|---|
| Swift (macOS/iOS) | Available | keylight-swift |
| Rust (CLIs/daemons/Tauri) | Available | keylight-rust |
| JavaScript/TypeScript | Available | keylight-js |
| C# (this repo) | Available | keylight-csharp |
| Unity (UPM) | Available | keylight-csharp — unity/dev.keylight.sdk |
| C++ | Planned | unified by the same cross-SDK conformance vectors |
About Keylight
Keylight is the licensing layer for desktop apps. You keep your own Stripe account, your own pricing, and your own customers — Keylight issues the licenses and tells your app who is allowed to run it.
- License keys issued automatically when a payment completes
- Device activations with limits you set, and self-serve deactivation
- Offline validation — signed Ed25519 leases your app verifies locally
- Feature entitlements signed into the lease, so tiers work offline too
keylight.dev · Documentation · Pricing
Further reading
- Licensing a Cross-Platform App from One Control Plane
- License Your Unreal Engine Game Offline in an Afternoon
- One-Time vs Subscription Licensing: Which to Use?
License
MIT License. See LICENSE for details.
<sub>Keylight C# SDK — software licensing for .NET: license-key activation & validation, offline Ed25519 lease verification, entitlement/feature gating, trials, and pluggable storage/transport — for .NET, Godot 4 (NuGet), and Unity (UPM coming).</sub>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.