trek-ai-license
v0.1.35
Published
Offline license verification for Trek AI UI (ECDSA P-256 signed, JWT-like format)
Readme
trek-ai-license
Offline license verification for Trek AI UI — ECDSA P-256 signed, JWT-like format, zero network calls.
This package is consumed by Trek AI UI itself (withInstall wires the guard into every component). Application developers normally never import it directly; it is documented here for vendors and advanced integrations.
1. How it works
A license is a single string with two dot-separated base64url parts:
<base64url(payloadJson)>.<base64url(signature)>- payloadJson — canonical JSON (fixed key order, see below) of the license payload.
- signature — 64-byte compact ECDSA P-256
(r||s)over the payload bytes, SHA-256 prehashed.
The normal flow requires zero license configuration on the consumer side:
- The vendor sets the expiry in
.env.license(see §4.1). ai-agent buildsigns the license at build time and embeds it into every bundle as__TREK_EMBEDDED_LICENSE__.- The consumer installs with plain
app.use(TrekAiUI)— the embedded license is picked up automatically.
At runtime the verification pipeline runs, in order:
format → signature → expiry → domain bindingVerification is synchronous and pure JS (noble-curves, no WebCrypto), so it also works on non-secure contexts (http:// intranet deployments) where crypto.subtle is unavailable. The result is final before the first component ever renders.
Payload (canonical signing order)
The key order below IS the signing canonical order. The issuing CLI and this runtime must both construct payloads in exactly this order, otherwise signatures will not match.
| Field | Type | Required | Description |
| ------------ | ---------- | -------- | -------------------------------------------------- |
| product | string | yes | Fixed identifier, must be trek-ai-ui |
| licenseId | string | yes | Unique id, e.g. LIC-2026-000123 |
| customer | string | yes | Customer / company identifier (diagnostics only) |
| issuedAt | string | yes | ISO date the license was issued |
| expiresAt | string | yes | ISO date the license expires (inclusive) |
| domain | string[] | no | Hostname patterns: acme.com, *.acme.com, * |
| features | string[] | no | Feature flags for tiered products, e.g. pro-table|
| type | string | no | trial | standard | enterprise | internal|
2. Security model
Offline by design. There is no license server and no network call. Trust is anchored in a single vendor public key baked into this package (src/keys.ts); the private key never leaves the vendor machine.
Key management
ai-agent license keygen- Generates the P-256 signing keypair once.
- The private key is written outside VCS (default
~/.trek-ai/license-keys.json,chmod 600). - Only the 65-byte uncompressed public point (hex) is committed into
src/keys.tsand shipped to consumers.
Enforcement gate
app.use(TrekAiUI)callssetupLicense(), which verifies the license and stores the accepted state in a plain (non-reactive) runtime object.- Every component's
setupis wrapped once byapplyLicenseGuard(installed bywithInstall), so the per-component runtime cost when licensed is a single function call plus a few field reads. - While the app stays open, expiry is re-evaluated every 60 s — a valid license that expires mid-session is caught without a reload.
Clock tamper detection
The largest wall-clock timestamp ever observed is persisted in localStorage (__TREK_LICENSE_MAX_TS). If the clock ever reads more than 2 minutes behind that marker, verification fails with status tampered. This defeats the naive "set the clock back to extend a trial" attack without requiring a server.
Domain binding
If the payload carries domain patterns, the runtime compares them against location.hostname:
acme.com— exact match*.acme.com— any subdomain, and the apex domain itself*— anywhere
On SSR / Node (no location), domain checks are skipped.
Grace window
setupLicense tolerates a verifying status for up to graceMs (default 3000 ms in production). Since verification is synchronous this window only guards against pathological main-thread stalls — status is effectively final before the first render.
3. Enforcement modes
| Mode | Behavior when license is missing / invalid / expired |
| ------------- | ------------------------------------------------------------- |
| warn | Render proceeds normally; a single console.warn is logged. |
| placeholder | Component is replaced by a blocked notice (el-license__blocked). Default. |
| throw | Throws during setup — hard kill, strongest deterrent. |
The gate itself is only active when required is true. setupLicense defaults required to process.env.NODE_ENV === 'production', so development and test builds are never blocked; production builds always enforce. Callers can force either direction with the required option.
Status lifecycle
unverified → verifying → valid → (expired | tampered)
↘ invalid | signature | domain-mismatchunverified— no license was passed toapp.use().expired— a previously valid license passed itsexpiresAt(caught on re-check).tampered— clock rollback detected.- The blocked notice exposes the raw status via
data-license-statusfor styling/telemetry.
4. For vendors: issuing licenses
4.1 Build-time embedding (the default path)
The vendor configures the license once in an env file — the expiry is part of the signed payload, so it cannot be edited in a shipped bundle without invalidating the signature (only the private key can re-sign):
# .env.license (in this package, or <project root>, or ~/.trek-ai/license.env)
TREK_LICENSE_EXPIRES_AT=2027-09-21 # bare date = end of that UTC day
#TREK_LICENSE_TTL_DAYS=365 # relative alternative when EXPIRES_AT is unset
TREK_LICENSE_CUSTOMER=trek-ai-ui
TREK_LICENSE_TYPE=release
#TREK_LICENSE_DOMAIN=* # comma-separated hostname patterns; `*` = no binding
#TREK_LICENSE_FEATURES= # comma-separated feature flags
#TREK_LICENSE_LICENSE_ID= # fixed id; a random LIC-<year>-<n> per build otherwiseConfig lookup order (first existing file wins; TREK_LICENSE_* process env vars override file values):
$TREK_LICENSE_ENV_FILE— explicit path<project root>/.env.license— the repo being built<trek-ai-license package>/.env.license— the canonical in-repo copy~/.trek-ai/license.env— machine-wide vendor config
Then just build the UI:
ai-agent buildThe build toolchain (trek-ai-build) signs the license from this config with the private key and injects it into the es/lib modules and the full bundles as the __TREK_EMBEDDED_LICENSE__ constant. If the config or the private key is missing, the build prints a warning and ships unlicensed (dev builds stay unaffected).
4.2 Manual issuance (ai-agent license issue)
# One-time: generate the signing keypair
ai-agent license keygen
# Issue a license — --customer/--expires fall back to the same .env.license
# config when omitted (prints the license string, or save with --out)
ai-agent license issue \
--customer=acme-corp \
--expires=2027-12-31 \
--domain=acme.com --domain=*.acme.com \
--features=pro-table --features=pro-form \
--type=standardA bare --expires=2027-12-31 means "valid through the end of that day (UTC)" (...T23:59:59.999Z).
Verify any license offline against the stored keypair:
ai-agent license verify "<license-string>"
ai-agent license verify @path/to/license.txtExit codes: 0 valid · 1 error / bad signature · 2 signature valid but expired.
5. For consumers: activating Trek AI UI
No license configuration is needed — the vendor-signed license is embedded at build time:
import { createApp } from 'vue'
import TrekAiUI from 'trek-ai-ui'
app.use(TrekAiUI)
app.mount('#app')The license option remains available as an override (e.g. per-customer licenses in the future):
app.use(TrekAiUI, {
license: '<license string issued by the vendor>', // optional override
// licenseMode: 'placeholder', // 'warn' | 'placeholder' | 'throw'
})Notes:
- Resolution order: explicit
licenseoption → the license embedded at build time. With neither, production builds still enforce — every component renders the blocked notice with statusunverified(dev/test builds are never blocked). - License texts are localized through Trek AI UI's own i18n (
el.license.*keys inen-US/zh-CN); the package ships English fallbacks when no resolver is wired. - For diagnostics / UI badges:
import { getLicenseInfo } from 'trek-ai-ui'
const { status, mode, required, payload } = getLicenseInfo()6. API reference
verifyLicense(license, options?)
Standalone verification (no global state touched). Returns a discriminated result:
type VerifyResult =
| { ok: true; payload: LicensePayload }
| { ok: false; reason: 'signature' | 'expired' | 'domain-mismatch' | 'malformed' }Options: publicKeyHex (tests / staged key rotation), ignoreExpiry (tests), now (injectable clock for tests).
setupLicense(options)
Initialize the global gate (called by the Trek AI UI installer).
interface SetupLicenseOptions {
license: string
mode?: 'warn' | 'placeholder' | 'throw' // default 'placeholder'
publicKeyHex?: string // tests / key rotation
required?: boolean // default NODE_ENV === 'production'
graceMs?: number // default 3000
}assertLicensed()
Synchronous per-component gate. Returns true when rendering may proceed (warn mode) and false when the caller should render the blocked notice (placeholder mode); throws in throw mode.
applyLicenseGuard(component)
Wraps a compiled component definition so unlicensed renders are replaced by the blocked notice. Idempotent (__licenseGuarded marker); non-component objects pass through untouched.
setLicenseTextResolver(resolver | null)
Wire a text resolver for the five license messages (expired, invalid, missing, domainMismatch, tampered). Trek AI UI's installer binds this to its own locale system.
Payload codec
bytesToBase64Url, base64UrlToBytes, parseLicenseString — the wire-format codec, exported for tooling that needs to inspect or mint licenses.
7. Key rotation
- Run
ai-agent license keygen --out=<new-keys.json>. - Stage the new public key: build consumers against a patched
src/keys.tsfirst (or passpublicKeyHexduring rollout). - Re-issue licenses with the new private key and swap the embedded public key in a library release.
Old licenses signed with the previous key stop verifying as soon as the new public key ships.
8. Limitations & recommendations
- Frontend JS can always be patched by a sufficiently motivated attacker — client-side enforcement raises the bar (expired/invalid copies render a blocked notice, signatures bind licenses to customers and domains), but it cannot be made unbreakable.
- For copy protection that truly resists tampering, pair this with a server-side entitlement check (the license
licenseId/customer/featuresfields are designed to be validated by your backend when the app calls home). - The private signing key is the root of trust: keep it in
~/.trek-ai/(or a secrets manager), never commit it, and rotate it if it leaks.
License
MIT
