crates.io License: Apache 2.0 keylight.dev
The open-source command-line client for the Keylight management API —
think wrangler, but for your Keylight tenant: apps, key types, licenses, customers, and
payment integrations, from the terminal instead of the dashboard.
Every endpoint the API exposes (OpenAPI spec) is reachable from a command, and a coverage test fails the build if that stops being true.
Status: 0.x. Flag names and human-readable output may change in a minor release. The
--jsonoutput and the exit codes are the parts to script against.
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.
- One command surface for the whole tenant. Apps, key types, licenses, customers, and payment integrations — every operation the dashboard exposes, scriptable from the terminal or a coding agent.
- RFC 8628 device-code login.
keylight loginprints a code, opens the browser to approve it, and works over SSH and on headless machines — the browser never needs to reach the CLI. - Confirm-gated by default. Destructive operations require an out-of-band browser approval, so a leaked token can't quietly delete anything.
brew tap keylight-dev/tap brew trust keylight-dev/tap brew install keylight
No Rust toolchain, and nothing to clear past Gatekeeper. The brew trust step is not specific
to this tap — since Homebrew 6, loading a formula from any third-party tap fails until you trust
it. The formula is 60 lines of Ruby
and worth reading before you trust it.
cargo install keylight-cli
Installs a keylight binary. Requires Rust 1.85+ (edition 2024). The crate is keylight-cli;
the binary is keylight.
Download from Releases for macOS (Apple Silicon), Linux (x86_64), or Windows (x86_64).
macOS: the release binaries are not yet code-signed or notarized, so Gatekeeper will block them on first run — clear the quarantine flag with
xattr -d com.apple.quarantine ./keylight. Homebrew andcargo installboth avoid this entirely, which is why they come first.
keylight login # approve in your browser keylight products list # confirm it works keylight licenses list --limit 10
keylight login is the easy path. For CI and agents, set an environment variable instead.
keylight login
Prints a short code, opens your browser to approve it, and stores the token when you do. Uses RFC 8628 device authorization, so it works over SSH and on headless machines — the browser never needs to reach the CLI.
Requests every non-destructive permission by default. Narrow it with --scope, repeated:
keylight login --scope licenses:read --scope usage:read
You can also uncheck permissions on the approval page. danger (destructive operations) is
never requested by default and must be opted into there.
Check the code on the approval page matches the one in your terminal. That match is what proves the request came from the session you started, and it is the reason the flow is safe on a machine you do not control.
| Source | Notes |
|---|---|
KEYLIGHT_API_TOKEN |
Always wins. Nothing is read from or written to disk. Use this in CI. |
| Stored config file | Written by keylight login or keylight auth set-token. |
export KEYLIGHT_API_TOKEN=klm_...
keylight auth statusTo store a token by hand:
keylight auth set-token # paste, then Enter echo "$KEYLIGHT_TOKEN" | keylight auth set-token # or pipe it
Read from stdin, never a command-line argument — arguments land in shell history and are
visible to other users via ps. Stored at your platform's config directory
(~/.config/keylight/config.toml on Linux, ~/Library/Application Support/keylight/config.toml
on macOS) with 0600 permissions.
Create tokens from the dashboard under Settings → API tokens.
keylight auth status # which tenant, token, and scopes the current auth resolves to keylight logout # revoke the token server-side and delete the local copy
logout revokes server-side and removes the local file. If the revoke call fails — offline, or
the token was already revoked — the local copy is still deleted and the output says so, rather
than leaving a credential on disk you believe is gone.
keylight init # detect the project, recommend SDK or API keylight init --mode api # wire up API mode: mint a token, write .env keylight init --mode sdk # print the SDK setup steps instead
init looks at the project and forks on integration style. A native app (Tauri, Xcode,
Cargo) is pointed at an SDK; a server (Next.js / Node) is pointed at the API. The
recommendation is advice — --mode always wins, and a browser/client project that picks API
anyway is warned (never embed a licenses:runtime token in code you ship), never blocked.
The API branch mints a licenses:runtime credential through the browser device flow, writes it
to .env as KEYLIGHT_API_TOKEN (owner-only, added to .gitignore in a git repo), and prints
a copy-ready curl. The token is the app's server-side secret — it is never stored in the CLI's
own config.
keylight products list keylight products get my-app keylight products create --display-name "My App" --key-prefix MYAP --support-email help@myapp.com keylight products create --display-name "My API App" --key-prefix MYAP \ --support-email help@myapp.com --integration-mode api keylight products update my-app --purchase-url https://myapp.com/buy keylight products free-tier set my-app --enabled keylight products test-mode disable my-app
ID NAME PREFIX FREE TIER TEST MODE
my-app My App MYAP true -
other Other App OTHR - true
A - means the API did not return a value for that column. It omits these flags entirely when
they are off rather than sending false, so an off flag reads as -, not false.
Enabling Stripe test mode needs two secrets, read from environment variables you name:
export KEYLIGHT_STRIPE_TEST_SECRET_KEY=sk_test_... export KEYLIGHT_STRIPE_TEST_WEBHOOK_SECRET=whsec_... keylight products test-mode enable my-app
keylight products key-types list --product my-app keylight products key-types create --product my-app \ --key-type-id pro --display-name "Pro" --activation-limit 3 --duration-days 365 keylight products key-types update pro --product my-app --activation-limit 5 keylight products key-types delete pro --product my-app # needs browser approval keylight products key-types verify-prices --product my-app
--duration-days 0 means perpetual. Any other number is a real expiry in days.
keylight licenses list --product my-app --status active --limit 20 keylight licenses get <license-id> keylight licenses create --product my-app --key-type pro \ --customer-email buyer@example.com --send-email keylight licenses remint <license-id> --send-email keylight licenses deactivate-device <license-id> --instance-id <instance> keylight licenses revoke <license-id> # needs browser approval
KEY APP TYPE STATUS DEVICES EXPIRES
MYAP-****-****-****-7K2QP my-app pro active 1/3 2027年06月18日
Raw license keys are never returned by the API — lists and detail views show the masked form.
The one exception is remint, which returns the plaintext replacement key once; the command
prints it and says so.
create and import always send an Idempotency-Key, so a retry after a timeout cannot issue
a second license to the same customer. Pass --idempotency-key to control it yourself.
Bulk import takes a JSON file — either an array of rows, or an object with a rows array:
keylight licenses import --product my-app --file rows.json --migration-source gumroad
Export writes CSV to stdout so it composes, or to a file with --output:
keylight licenses export > licenses.csv # needs browser approval keylight licenses export --output licenses.csv
keylight customers list --limit 20 keylight customers get <customer-id> keylight customers create --email buyer@example.com --name "Buyer" keylight customers link-license <customer-id> --license <license-id> keylight customers entitlements set <customer-id> \ --license <license-id> --entitlements '["sync","export"]'
A customer id is a SHA-256 hash of their email, so get takes the id that list prints.
keylight integrations list keylight integrations status # Stripe connection detail keylight integrations connect stripe --mode live # returns an OAuth URL to open keylight integrations disconnect paddle --environment sandbox keylight integrations rotate-secret paddle # needs browser approval
Non-Stripe providers take credentials from environment variables you name:
export PADDLE_KEY=pdl_live_...
keylight integrations connect paddle --environment live --api-key-env PADDLE_KEYkeylight webhook get keylight webhook set --url https://example.com/keylight # needs browser approval keylight webhook set --url "" # clears it; delivery stops keylight sdk-key get # needs browser approval keylight sdk-key rotate # needs browser approval keylight usage --days 30 keylight test-purchase create --product my-app --key-type pro keylight test-purchase get <run-id>
webhook set returns the signing secret — use it to verify the signature on every delivery.
webhook get only reports whether a secret exists, never its value.
KEYLIGHT_SDK_KEY=sdk_live_... keylight setup verify-license --product my-app keylight setup verify-license --clean
Issues a real test license, activates it through the Rust SDK, and verifies the
returned lease against the tenant keyset. It does not modify application code
or count as product usage. The check is identified as Setup check; a local,
owner-only state file retains its plaintext key only until --clean removes
the device and revokes the license.
export KEYLIGHT_RUNTIME_TOKEN=klm_... # the app's licenses:runtime token keylight setup verify-api --product my-app
The HTTPS-only counterpart to verify-license — no SDK involved. It mints a test license with
your CLI login, then activates and validates it through the /v1/licenses runtime endpoints
using your server-side runtime token, prints the JSON, and cleans up (unless --keep). This is
the exact path an API-mode integration takes. The runtime token is read from
KEYLIGHT_RUNTIME_TOKEN (override with --token-env) — kept distinct from the CLI's own
KEYLIGHT_API_TOKEN management auth, which mints and cleans up.
Every command supports --json, which prints the raw API response — that is the contract to
script against, and it is stable in a way the human output is not.
keylight --json licenses list --product my-app | jq '.items[].displayKey'
Errors follow the same rule: --json prints {"error": "..."} to stdout; otherwise the message
goes to stderr. Progress output (approval URLs, waiting notices) always goes to stderr, so
stdout stays parseable and redirects stay clean.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Everything else |
2 |
Needs action: authenticate, or supply the missing setup input. |
3 |
A confirmation was denied or expired |
2 is distinct because it marks a failure an agent can act on without treating a
verification failure as something to work around.
Some operations are dangerous enough that a bearer token alone cannot run them — a human has to approve the specific call in a browser (same shape as npm's web-based 2FA). When a command hits one, the CLI:
- Prints an approval URL (and opens it, unless
--no-browseror stdout is not a terminal). - Polls until you approve or deny it, or it expires after 10 minutes.
- Retries the original call automatically once approved.
Seven operations are gated: licenses revoke, licenses export, products key-types delete,
integrations rotate-secret, sdk-key get, sdk-key rotate, and webhook set. You never have
to know which — the CLI walks the flow whenever the server asks.
These block and wait by design. They are human-gated operations, and failing fast for unattended callers would misrepresent what they are; an agent can surface the URL to its user.
| Env var | Effect |
|---|---|
KEYLIGHT_API_TOKEN |
Bearer token; wins over the stored config file. |
KEYLIGHT_API_URL |
API base URL (default https://api.keylight.dev). For local dev and staging. |
Global flags: --json, --no-browser (never auto-open a browser; URLs are still printed),
--api-url.
No command ever requires an interactive prompt. Every input is available as a flag, so an
agent or CI job can drive the whole surface. Secrets are the one exception to "as a flag": they
are read from environment variables you name, because argv lands in shell history and is visible
to other users via ps.
Pagination is explicit. list commands take --limit and --cursor and print the next
cursor when one exists. Nothing auto-pages — one command means one request, and a silent loop
over a large tenant is both surprising and expensive.
--product is required wherever the endpoint is product-scoped, never inferred from config.
Inferring it would make the same command mean different things on different machines.
Commands are hand-written, not generated from the OpenAPI spec. Help text, flag names, output
formatting and error messages are the entire value over curl. Drift protection comes from the
coverage test instead, which catches the failure that actually matters — an endpoint no command
can reach.
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo testTests mock the HTTP transport (no network needed) and isolate the config path, so cargo test
never reads or writes your real config.
The coverage test reads a checked-in snapshot of the API spec at tests/fixtures/openapi.json
rather than fetching it, so the suite is deterministic and works offline. Refresh it when the
API changes:
scripts/refresh-openapi.sh
Drift then shows up as a reviewable diff instead of a surprise failure, and the test reports any endpoint that no command reaches.
keylight-swift— Swift SDKkeylight-rust— Rust SDK + Tauri pluginkeylight-js— TypeScript/JavaScript SDKkeylight-csharp— C#/.NET SDK
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. This CLI is how you manage all of that from a terminal instead of the dashboard.
- 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
Apache-2.0 — see LICENSE.