@aikdna/kdna-activation-server
v0.2.1
Published
Self-hostable KDNA entitlement service for license activation, synchronization, revocation, and signed status records.
Maintainers
Readme
@aikdna/kdna-activation-server
Experimental self-hostable HTTP activation server for KDNA licensed assets.
KDNA makes judgment portable across models and runtimes. This repository is an experimental entitlement reference implementation, not a marketplace, billing service, or AIKDNA-hosted activation platform.
This package implements the legacy license-key and signed-receipt profile. It is not an RFC-0019 account/device external-key-grant issuer. Implementations must not present one profile as the other.
The registry package at 0.2.0 is the published baseline. Repository
0.2.1 is an unreleased source candidate containing the secret-input and
verifier-at-rest corrections below; it is not npm latest and a checkout is not
evidence of publication.
This server answers one question:
Is this user / device / organisation currently entitled to use this asset?
It implements the four endpoints in
specs/kdna-entitlement-api.md and the self-hosting invariant
from docs/REMOTE_MODE.md.
The responsibility routes documented below are the only public HTTP contract.
In particular, Remote 0.4.1 and later send entitlement refreshes to
/entitlements/sync; deploy Activation 0.2.0 before Remote 0.4.1 or later.
The server validates every entitlement domain against the authoritative
asset_id grammar shipped by KDNA Core 0.21.0.
Self-hosting is the default
The KDNA protocol MUST NOT assume a single official KDNA server. Any asset creator can run their own activation server. No AIKDNA-hosted activation service is part of the current public baseline.
This server is the deployer's own. The protocol does not hardcode any KDNA Inc. URL. The admin token is deployer- controlled. License records are deployer-controlled. The server's signing keypair is generated on first start and stored locally.
Quick start (self-hosting)
# 1. Start from a trusted exact 0.2.1 source checkout on Node 18+.
npm ci
npm test
npm pack
npm install -g ./aikdna-kdna-activation-server-0.2.1.tgz
# 2. Create private input files without placing secrets in shell arguments.
install -m 600 /dev/null ./license-request.json
${EDITOR:?Set EDITOR} ./license-request.json
kdna-activation-server --create-license-file ./license-request.json
rm ./license-request.json
install -m 600 /dev/null ./admin-token
${EDITOR:?Set EDITOR} ./admin-token
# 3. Start the server. The token file must remain private.
kdna-activation-server --port 3001 --admin-token-file ./admin-token
# 4. Test. Create the request body with a private editor, not inline argv.
curl http://localhost:3001/healthz
install -m 600 /dev/null ./activation-request.json
${EDITOR:?Set EDITOR} ./activation-request.json
curl -X POST http://localhost:3001/entitlements/activate \
-H 'Content-Type: application/json' \
--data-binary @./activation-request.json
rm ./activation-request.jsonThat's it. No registration, no phone-home, no KDNA Inc. URL.
Do not replace a placeholder with a real secret inside a command argument.
--create-license-stdin and --admin-token-stdin are available when a
deployer's secret provider can pipe bounded strict UTF-8 directly.
The CLI rejects unknown options, unexpected positional values, duplicate
options, cross-mode combinations, and values attached to boolean stdin flags;
these errors never echo the rejected token.
HTTP API
GET /healthz
Health check. Returns 200 with server metadata.
GET /server/identity
Returns the server's Ed25519 public key (PEM, hex, and fingerprint). Clients use this to verify that an entitlement record was really signed by this server.
POST /entitlements/activate
Activates a license. Returns a signed entitlement record
(cryptographically verifiable against /server/identity).
Request body:
{
"domain": "kdna:yourname:your-asset",
"license_key": "<license-secret>",
"machine_fingerprint": "<sha256>"
}Optional: client, client_version, agent, account_id,
device_label.
machine_fingerprint is required when the license was created with
require_machine_binding: true (the default). Its canonical wire format is
exactly 64 lowercase hexadecimal characters: the SHA-256 digest produced by
the client. Uppercase, prefixed, whitespace-padded, non-ASCII, short, and long
forms are rejected rather than normalized into aliases.
Response (200): the signed entitlement record (see
specs/kdna-entitlement-api.md §5). The response and its signed body never
contain license_key; clients only send that secret in activation and sync
request bodies.
domain is the entitlement contract field for the Core manifest asset_id.
Its value must satisfy the canonical asset identity grammar from Core 0.21.0's
published manifest.schema.json. No alternate package-name syntax is accepted
as a second identity format.
Errors:
INVALID_LICENSE_KEY(404) — key does not match the domainLICENSE_REVOKED(403) — license has been revokedLICENSE_EXPIRED(403) —expires_atis in the pastMISSING_MACHINE_FINGERPRINT(400) — a bound license omitted its fingerprintINVALID_MACHINE_FINGERPRINT(400) — the fingerprint is not canonicalMACHINE_MISMATCH(403) — the license is bound to another machine or has not yet been activated on this machine
POST /entitlements/sync
Refreshes the entitlement state (updates last_checked_at and
offline_valid_until). domain and license_key are required;
license_id is optional but, when present, must identify that same license.
Machine-bound licenses must already have been activated and must send the same
canonical machine_fingerprint. Returns the signed record. Same errors as
/activate.
POST /entitlements/revoke (admin)
Revokes a license. Requires an Authorization: Bearer
<admin-token> header. The admin token is set at server
startup.
Request body:
{
"license_id": "lic_abc123",
"domain": "kdna:yourname:your-asset",
"reason": "payment_failed",
"revoked_by": "billing-system"
}GET /entitlements/status?domain=...&license_id=...&machine_fingerprint=...
Introspection. Returns public entitlement metadata (unsigned,
for introspection only) and does not include license_key.
Both the canonical scoped domain and license_id are required. The status
endpoint rejects license_key entirely so the secret cannot appear in URLs or
access logs; use /activate or /sync for signed entitlement records.
For machine-bound licenses, status requires the already-bound canonical
fingerprint and never creates a first binding. Error responses do not include
license metadata, the submitted fingerprint, or the stored binding digest.
Missing, malformed, mismatched, and not-yet-bound machine authorization all
return the same NOT_FOUND response as an unknown record, so public
license_id values cannot be used as a binding-enumeration oracle.
CLI
# Create a license (one-shot) from a private request body
kdna-activation-server --create-license-stdin < ./license-request.json
# or: kdna-activation-server --create-license-file ./license-request.json
# List all licenses
kdna-activation-server --list
# Revoke
kdna-activation-server --revoke lic_abc123 --reason "payment_failed"
# Start the server with one private token source
kdna-activation-server --port 3001 --admin-token-file ./admin-token
# or: kdna-activation-server --port 3001 --admin-token-stdinThe server keypair is auto-generated on first start and
stored at ~/.kdna/activation-server/. The private key is
mode 0600.
Security properties
- No KDNA Inc. URL is hardcoded. The server has zero outbound network calls during normal operation.
- The server keypair is local. The private key never leaves the deployer's machine.
- The admin token is deployer-controlled. Set it at
startup through bounded strict UTF-8 stdin or a private regular file, or omit
it to disable
/revokeover HTTP. Raw--admin-tokenargv is rejected. - The license_key is a request secret. It is accepted only in activation
and sync JSON request bodies. The server does not return it in signed
records, status responses, errors, or command output. Clients should not
place it in URLs, argv, or logs. License creation reads private JSON through
stdin or a private file; raw
--create-licenseargv is rejected. - License secrets are verifier-only at rest. New records store an
independently salted, bounded-parameter scrypt verifier, never the plaintext
license_key. Verification is constant-time. A legacy plaintext record is atomically rewritten only after the caller supplies the exact old secret; failed verification or concurrent drift leaves the original bytes intact. Salt and verifier bytes are server-only and are not usable as a license key. - Records are signed. Every
/activateand/syncresponse is signed with the server's Ed25519 key. Clients can verify against/server/identity. - Raw machine fingerprints are not stored by new activations. The server derives a purpose-separated HMAC key from its local private key and stores only the keyed binding digest. A matching request migrates an older raw fingerprint record in place; malformed legacy bindings fail closed.
- License record filenames are collision-free. Each validated license identifier has one encoded storage path. Exact legacy records migrate on write, while a different identifier that shared an older sanitized filename is never treated as an alias. Directory scans only discover identifiers; activation, listing, and key lookup always re-read the authoritative path.
- HTTP routing is origin-form only. Requests require one syntactically
valid
Hostheader, while route selection uses a fixed internal base rather than the supplied host. Absolute request targets and Host values containing credentials, paths, queries, or fragments are rejected. - JSON bodies are byte-bounded and strictly decoded. Activation, sync, and revocation accept at most 64 KiB of UTF-8 bytes. Oversized, malformed UTF-8, and malformed JSON inputs receive stable errors without parser details.
Local development
git clone https://github.com/aikdna/kdna-activation-server
cd kdna-activation-server
npm testThe tests spin up the server on an OS-assigned port. No external services are required.
License
Apache 2.0. See LICENSE.
This server is a license-management reference implementation. Trust is the consumer's decision, not the server's claim.
