Integrate the unchanged JavaScript SDK with an explicit validity gate and a stable private installation state.
What you will learn
- Install the actual source package and configure every constructor option.
- Use the complete session CLI and distinguish release success from feature access.
- Understand every exposed member, storage/error/offline boundary and packaging limit.
Prerequisites
- Complete the Sandbox quickstart.
- Read signed responses and use only synthetic Sandbox data.
Runtime and package
Node18+ ESM with native fetch/crypto/filesystem APIs; the tested session program targets Node18.20+ syntax and runs on Node22.22.2. Intended for Node services or Electron main process, not ordinary browser JavaScript.
Package @kdpika/license-sdk, current label 1.0.0. Download kdpika-javascript-sdk-1.0.0.zip from the SDK overview and verify SHA-256 before extracting. This is a local source install, not a claim that a public registry release exists.
Install and use
Run the installation command in an empty application project with the extracted SDK directory. Set KDP_APP_ID and a private KDP_STATE_DIR. For PHP also set KDP_SDK_DIR to the extracted package directory. For activation set KDP_LICENSE_KEY and optional KDP_CUSTOMER_EMAIL from trusted user input/your secret environment, not source literals.
npm install --ignore-scripts --save-exact ./kdpika-javascript-sdk
Expected: the exact local SDK installs/builds and its imports resolve. Python on Windows uses .venv\Scripts\python.exe instead of .venv/bin/python; the transcript is the tested macOS path. No registry release is implied by dependency restoration.
The session program is complete and runs one explicit action per invocation, preserving state across processes. Its SDK constructor deliberately sets SANDBOX. The key-prefix/URL checks are demonstration guards, not a replacement for server environment lookup or SDK verification. HTTPS is the default; local fixture HTTP requires KDP_ALLOW_LOOPBACK=1.
/** Sandbox session CLI; Node18.20+, official local SDK1.0.0 (ESM).
* Run node session.mjs status|activate|check|validate|deactivate.
* Set KDP_APP_ID,KDP_STATE_DIR; activate: KDP_LICENSE_KEY/optional customer email.
* Expected safe JSON, no raw signed payload/activation token. Keep validity gate.
*/
import crypto from 'node:crypto';
import {LicenseClient} from '@kdpika/license-sdk';
function required(name){const value=(process.env[name]||'').trim();if(!value)throw Error('Missing configuration');return value;}
try{
const action=process.argv.length===3?process.argv[2]:'';
if(!['status','activate','check','validate','deactivate'].includes(action))throw Error('Choose an action');
const base=(process.env.KDP_BASE_URL||'https://licensing.kdpika.com').replace(/\/+$/,'');const uri=new URL(base);
const local=uri.protocol==='http:'&&['127.0.0.1','localhost'].includes(uri.hostname)&&process.env.KDP_ALLOW_LOOPBACK==='1';
if(uri.username||uri.password||uri.pathname!=='/'||uri.search||uri.hash||(uri.protocol!=='https:'&&!local))throw Error('Use an HTTPS site origin');
const client=new LicenseClient({product:required('KDP_APP_ID'),environment:'SANDBOX',baseUrl:base,
applicationName:'KDPika SDK session example',applicationVersion:'1.0.0',storageDir:required('KDP_STATE_DIR')});
if(action==='status')console.log(JSON.stringify({activated:client.activated,storage_security:'permission-protected-file',
installation_hash:crypto.createHash('sha256').update(client.installationId).digest('hex')}));
else{
let result;
if(action==='activate'){
const key=required('KDP_LICENSE_KEY');if(!/^TEST[A-Z0-9]*-/i.test(key))throw Error('Use a Sandbox key');
result=await client.activate(key,process.env.KDP_CUSTOMER_EMAIL||null);
}else result=await client[action]();
const active=action!=='deactivate'&&result.valid&&result.signatureValid;
const limit=active?result.featureValue('seat_count'):null;
console.log(JSON.stringify({valid:result.valid,source:result.source,error_code:result.errorCode,
signature_valid:result.signatureValid,pdf_export_allowed:Boolean(active&&result.feature('pdf_export')),
seat_count:Number.isSafeInteger(limit)&&limit>=0?limit:null,request_id:/^req_[a-f0-9]{24}$/.test(result.requestId||'')?result.requestId:null,
message:action==='deactivate'&&result.valid?'This installation is deactivated.':result.valid?'Your license is ready.':'Your license could not be confirmed. Check your key or contact the software publisher.'}));
if(!result.valid)process.exitCode=1;
}
}catch{
console.error('License check unavailable. Review configuration, connectivity and private storage permissions. Do not disable signature verification.');process.exitCode=2;
}
- Run node session.mjs status. Expected: activated=false for fresh storage, with only an installation hash and storage category.
- Run node session.mjs activate with a Sandbox key and matching email. Expected: valid=true/signature_valid=true or an explicit refusal, never a raw token/payload dump.
- Run node session.mjs check using the same state directory. Expected: current valid result from the eligible cache or online path; no feature is enabled on failure.
- Run node session.mjs validate. Expected: forced-online path, subject to the documented bounded transport-failure fallback.
- Run node session.mjs deactivate. Expected: the installation seat is released and local token/cache removed; pdf_export_allowed remains false for this release operation.
- Run node session.mjs status again. Expected: activated=false with the same installation hash. Cumulative activation_count is not reset.
The programs exit0 for an accepted operation/status, exit1 for a licensing refusal, exit2 for configuration/unexpected failure. They expose safe decisions only. seat_count is read only from a verified valid result and must be a nonnegative integer; an absent value remains null. Result feature helpers cannot substitute for the validity/signature gate.
Complete constructor configuration
| Option | Type | Default | Meaning |
|---|---|---|---|
| product | string | required | Public App ID; never the internal product ID or an lmp_ key. |
| environment | string | LIVE (Python: production maps to LIVE) | Set SANDBOX explicitly for examples. production/test aliases normalize to LIVE/SANDBOX. |
| baseUrl | string | https://licensing.kdpika.com | Site origin, without /api/v1; default HTTPS. Loopback HTTP is for local testing only. |
| applicationName | string | Licensed Application | Names the default application storage directory and device fallback label. |
| applicationVersion | string | empty string | Application release sent at activation. Empty version skips the server version-range check. |
| verificationKey | dictionary/array/object | null/None | Optional pinned key with key_id and raw base64url public_key; manual rotation. |
| storageDir | path/string | OS-derived application storage root | Namespace includes origin, product and environment; keep outside public web roots. |
| timeoutMs | seconds / milliseconds | 15000 milliseconds | Transport timeout, not an offline grace or validation interval. |
| fetchImpl | fetch-compatible function | globalThis.fetch | Test injection must preserve the actual response/error contract; never accept unsigned fixtures in release. |
| clock | callable | current UTC-compatible time | Testing clock; do not expose time control to untrusted application callers. |
Timeout is an HTTP timeout, not a client-side way to configure offline grace or validation interval. Those times come from the verified signed payload and the product configuration. Defaults are actual source defaults; the runnable sample explicitly changes environment/storage choices for safe isolated tests.
Public members and result contract
This inventory includes constructors, fields/properties, lifecycle/result helpers and exposed low-level members. JavaScript helpers are language-public even when they are implementation details. Python non-underscore storage/transport classes are listed rather than silently hidden. The exact source signature is shown so a guide cannot invent a method such as has_feature().
| Member | Kind | Exact signature / field | Use and return/error boundary |
|---|---|---|---|
| LicenseResult.constructor | constructor | constructor({valid, source, payload = {}, errorCode = null, message = "", requestId = null, signatureValid = false, storageSecurity = "permission-protected-file"}) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseResult.feature | method | feature(name, fallback = false) | Boolean-style feature lookup only. Never authorizes invalid results; C# true/false fallback semantics differ from truthiness in the other SDKs. |
| LicenseResult.featureValue | method | featureValue(name, fallback = null) | Read the typed feature value/default. Check valid and the actual feature type first. |
| LicenseClient.constructor | constructor | constructor({product, environment = "LIVE", baseUrl = "https://licensing.kdpika.com", applicationName = "Licensed Application", applicationVersion = "", verificationKey = null, storageDir = null, timeoutMs = 15000, fetchImpl = globalThis.fetch, clock = () => new Date()}) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseClient.activated | property | get activated() | Token-presence status only; not a current entitlement decision. |
| LicenseClient.activate | method | async activate(licenseKey, email = null) | Send activation; return a valid signed result or explicit refusal. Success stores the token and signed state; verification failure attempts best-effort deactivation. |
| LicenseClient.check | method | async check(forceOnline = false) | Use verified cache when eligible or validate online. A transport failure can enter bounded offline policy; explicit server rejection is not offline permission. |
| LicenseClient.validate | method | validate() | Force the check path online; transport failure can still attempt bounded verified offline fallback, not an unconditional online-only guarantee. |
| LicenseClient.deactivate | method | async deactivate() | Release this token seat online and clear token/cache on success. A successful release result does not grant access. Installation identity remains. |
| LicenseClient.feature | method | async feature(name, fallback = false) | Client helper may perform a new check but does not enforce valid itself. Prefer one accepted result, then result feature access. Never use the helper as the sole permission gate. |
| LicenseClient.offline | method | async offline() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.verified | method | async verified(signed, source, response = null) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.getVerificationKey | method | async getVerificationKey(signed) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.saveState | method | async saveState(signed) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.checkpoint | method | checkpoint() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.token | method | token() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.bestEffortDeactivate | method | async bestEffortDeactivate(token) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.request | method | async request(method, endpoint, body, headers) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.responseError | method | responseError(response) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.error | method | error(errorCode,message,source,response=null,payload={},signatureValid=false) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.now | method | now() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.readJson | method | readJson(name) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.writeJson | method | writeJson(name,value) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.atomicWrite | method | atomicWrite(name,value) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.remove | method | remove(name) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| module.canonicalJson | function | export function canonicalJson(value) | Exported current canonicalizer, with the documented integral-float limitation. Not a standalone verification or entitlement decision. |
| LicenseResult.valid | field | valid | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.source | field | source | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.payload | field | payload | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.errorCode | field | errorCode | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.message | field | message | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.requestId | field | requestId | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.signatureValid | field | signatureValid | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.storageSecurity | field | storageSecurity | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.product | attribute | product | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.environment | attribute | environment | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.baseUrl | attribute | baseUrl | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.applicationName | attribute | applicationName | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.applicationVersion | attribute | applicationVersion | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.verificationKey | attribute | verificationKey | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.timeoutMs | attribute | timeoutMs | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.fetchImpl | attribute | fetchImpl | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.clock | attribute | clock | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.storageDir | attribute | storageDir | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.installationId | attribute | installationId | Persisted random installation identity; not a hardware fingerprint or secret developer API key. |
A LicenseResult carries valid/source/payload/error code/message/request ID/signature status/storage category under the language-specific spelling above. Lifecycle calls return this result type. Unknown codes fail closed. HttpResponse/custom transport status is not an access grant. A field in a constructed result has not been verified merely because it is present.
The exact runnable session covers the real Sandbox lifecycle and safe output. Representative local/model/helper assertions execute every inventoried public member; the optional Python dialog has four genuine offscreen Qt cases. These are not claims that every branch, injected cancellation, native vault, framework or target OS was executed. The full app examples record actual packaged/native versus cross-build/controller boundaries explicitly.
Runnable member examples
This separate complete program exercises the public local/result/helper surface with explicit assertions and no access grant. It is an isolated conformance exercise, not an alternative licensing implementation. Run it beside the installed source package using node members.mjs. Expected: result=PASS and a list of exercised members, without credentials or raw customer payloads.
Python makes one read-only public-key GET to KDP_BASE_URL (default HTTPS production origin); its product is an unusable example App ID, and it does not activate. Its optional PyQt dialog is tested separately in four offscreen cases. PHP and C# use only synthetic local objects and no HTTP. JavaScript uses an ephemeral test signing key and injected synthetic transport, never a real KDPika credential or network response. The normal session example above is separately tested against the real isolated Sandbox.
These examples use their own fresh temporary directories. They never inspect or delete an existing installation. Result fields are constructed as invalid deliberately: a true feature helper cannot override valid=false. Typed return assertions demonstrate behavior, not complete branch, native vault or target-OS coverage.
// Isolated public-member conformance exercise, NOT a production access client.
// The ephemeral key signs synthetic in-memory data for an injected transport.
// It is never trusted by KDPika, written to disk, printed or used on the network.
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {LicenseClient, LicenseResult, canonicalJson} from "@kdpika/license-sdk";
const covered = [];
const check = (member, condition) => { assert.ok(condition, `Member example failed: ${member}`); covered.push(member); };
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "kdpika-members-"));
fs.chmodSync(temporary, 0o700);
try {
let now = new Date("2026-10-17T12:00:00Z"), mode = "online", releaseCalls = 0;
const pair = crypto.generateKeyPairSync("ed25519");
const publicBytes = pair.publicKey.export({type: "spki", format: "der"}).subarray(-32);
const key = {key_id: "ed25519_example", algorithm: "Ed25519", public_key: publicBytes.toString("base64url")};
let client;
const signed = () => {
const payload = {product_id: "YOUR_APP_ID", environment: "sandbox", status: "active",
device: {installation_hash: crypto.createHash("sha256").update(client.installationId).digest("hex")},
features: {pdf_export: true, seat_count: 2}, server_time: "2026-10-17T12:00:00Z",
next_validation_at: "2026-10-18T12:00:00Z", offline_grace_until: "2026-10-20T12:00:00Z",
expires_at: "2026-11-17T12:00:00Z"};
return {payload, signature: crypto.sign(null, Buffer.from(canonicalJson(payload)), pair.privateKey).toString("base64url"),
algorithm: "Ed25519", key_id: key.key_id};
};
const fetchImpl = async (url, options) => {
const endpoint = new URL(url).pathname;
assert.equal(new URL(url).origin, "https://licenses.example.invalid");
assert.ok(options.signal instanceof AbortSignal);
if (mode === "network") throw new Error("Synthetic transport outage");
let body;
if (endpoint === "/api/v1/public-key") body = {success: true, key};
else if (endpoint === "/api/v1/licenses/deactivate") { releaseCalls++; body = {success: true, deactivated: true}; }
else if (mode === "rejected") return {ok: false, status: 403, json: async () => ({success: false, error: {code: "LICENSE_REVOKED", message: "Example refusal"}})};
else body = {success: true, activation_token: "UNUSABLE_EXAMPLE_TOKEN", license: signed(), request_id: "req_example"};
// The actual SDK deliberately requires act_ at activation. This unusable,
// local-only test value cannot be accepted by a real KDPika token lookup.
if (endpoint === "/api/v1/licenses/activate") body.activation_token = "act_example_not_a_real_credential";
return {ok: true, status: 200, json: async () => body};
};
client = new LicenseClient({product: "YOUR_APP_ID", environment: "SANDBOX", baseUrl: "https://licenses.example.invalid",
storageDir: temporary, verificationKey: key, fetchImpl, clock: () => now});
check("LicenseClient.constructor", client instanceof LicenseClient);
for (const name of ["product", "environment", "baseUrl", "applicationName", "applicationVersion", "verificationKey",
"timeoutMs", "fetchImpl", "clock", "storageDir", "installationId"]) check(`LicenseClient.${name}`, client[name] !== undefined);
check("LicenseClient.activated", !client.activated);
check("LicenseClient.token", client.token() === null);
check("LicenseClient.now", client.now().toISOString() === now.toISOString());
check("module.canonicalJson", canonicalJson({b: [1, true], a: "value"}) === '{"a":"value","b":[1,true]}');
const example = new LicenseResult({valid: false, source: "example", payload: {features: {pdf_export: true, seat_count: 2}},
errorCode: "EXAMPLE_ONLY", message: "Example", requestId: "req_example", signatureValid: false, storageSecurity: "unknown"});
check("LicenseResult.constructor", Object.isFrozen(example));
for (const [name, expected] of Object.entries({valid: false, source: "example", errorCode: "EXAMPLE_ONLY", message: "Example",
requestId: "req_example", signatureValid: false, storageSecurity: "unknown"})) check(`LicenseResult.${name}`, example[name] === expected);
check("LicenseResult.payload", example.payload.features.pdf_export === true);
check("LicenseResult.feature", example.feature("pdf_export") && !example.feature("missing"));
check("LicenseResult.featureValue", example.featureValue("seat_count") === 2 && example.featureValue("missing", 7) === 7);
check("application.fail_closed", !(example.valid && example.signatureValid && example.feature("pdf_export")));
check("LicenseClient.error", client.error("EXAMPLE_ONLY", "Example", "local").valid === false);
check("LicenseClient.responseError", client.responseError({body: {error: {code: "INVALID_TOKEN", message: "Example"}}}).errorCode === "INVALID_TOKEN");
const response = await client.request("GET", "/api/v1/public-key", null, {});
check("LicenseClient.request", response.ok && response.status === 200 && response.body.key.algorithm === "Ed25519");
check("LicenseClient.getVerificationKey", (await client.getVerificationKey(signed())).key_id === key.key_id);
check("LicenseClient.verified", (await client.verified(signed(), "online")).signatureValid);
await client.saveState(signed());
check("LicenseClient.saveState", client.readJson("signed-state.json").signed_license.algorithm === "Ed25519");
client.checkpoint();
check("LicenseClient.checkpoint", client.readJson("signed-state.json").last_seen_at === now.toISOString());
check("LicenseClient.offline", (await client.offline()).valid);
client.writeJson("example.json", {local_example: true});
check("LicenseClient.writeJson", fs.existsSync(path.join(client.storageDir, "example.json")));
check("LicenseClient.readJson", client.readJson("example.json").local_example === true);
client.atomicWrite("example.txt", "unusable local text");
check("LicenseClient.atomicWrite", fs.readFileSync(path.join(client.storageDir, "example.txt"), "utf8") === "unusable local text");
client.remove("example.txt");
check("LicenseClient.remove", !fs.existsSync(path.join(client.storageDir, "example.txt")));
const activated = await client.activate("TEST-UNUSABLE-EXAMPLE", "synthetic@example.invalid");
check("LicenseClient.activate", activated.valid && activated.signatureValid && client.activated);
check("LicenseClient.check", (await client.check()).source === "cache");
check("LicenseClient.validate", (await client.validate()).source === "online");
check("LicenseClient.feature", await client.feature("pdf_export"));
const beforeRelease = releaseCalls;
await client.bestEffortDeactivate("UNUSABLE_EXAMPLE_TOKEN");
check("LicenseClient.bestEffortDeactivate", releaseCalls === beforeRelease + 1);
now = new Date("2026-10-18T13:00:00Z"); mode = "network";
assert.equal((await client.validate()).source, "offline");
now = new Date("2026-10-21T12:00:00Z");
assert.equal((await client.validate()).errorCode, "OFFLINE_GRACE_EXPIRED");
now = new Date("2026-10-17T11:54:59Z");
assert.equal((await client.offline()).errorCode, "CLOCK_ROLLBACK_DETECTED");
now = new Date("2026-10-19T12:00:00Z"); mode = "rejected";
assert.equal((await client.validate()).errorCode, "LICENSE_REVOKED");
assert.equal(client.activated, false);
mode = "online";
assert.equal((await client.activate("TEST-UNUSABLE-EXAMPLE")).valid, true);
const released = await client.deactivate();
check("LicenseClient.deactivate", released.valid && !released.signatureValid && !client.activated);
console.log(JSON.stringify({result: "PASS", members: covered, mode: "injected synthetic signing/transport; zero HTTP",
access_granted: false, additional_cases: ["offline", "grace-expired", "clock-rollback", "explicit-revocation"]}));
} finally {
// Only the exact fresh scratch directory from mkdtempSync is removed.
fs.rmSync(temporary, {recursive: true});
}
Errors and exceptions
Constructor validation throws Error. Lifecycle methods normally resolve LicenseResult; storage/custom-fetch/unexpected JSON failures can reject a Promise. Catch at your boundary and never show raw error text or payload. No built-in application logger or trace-redaction pipeline exists. The request helper uses AbortController; transport throws enter the documented offline/refusal path.
Use the complete error table for code/namespace/cause/correction and safe user messages. Log only application version, UTC time, action, code, source and a validated req_ correlation value. Never log the key, token, Authorization header, customer payload or an unredacted state file. No debug flag changes the signature or validity policy.
Storage and installation identity
Permission-protected files in a namespaced directory, with0700/0600 attempts where supported. No built-in Electron safeStorage, Windows Credential Manager or Secret Service adapter. Keep tokens in the main process and use the storage hardening guide. A JavaScript property or local payload is not trusted by itself.
Storage namespace is based on origin/product/environment. Keep those stable across app updates; changing them can create a separate identity/seat. Reinstalling or deleting identity can consume another cumulative activation even after a seat was released. Do not use machine serials/MAC addresses as a substitute for the persistent random ID.
Offline and clock policy
A new activation requires network. check uses verified signed state before next_validation_at when eligible. After that point it validates online; only a transport failure attempts the verified offline path. Explicit server rejection clears signed cache and selected invalid-token/device/revocation/expiry/cancellation errors also remove the stored credential. Offline/cache paths require active status, valid signature, product/environment/installation binding, expiry/grace and clock policy.
Clock rollback tolerance is300 seconds behind the latest trusted checkpoint/server time. A user controlling the host can patch the program or roll back a full machine snapshot; local checks are not absolute anti-tamper guarantees. Some malformed/missing signed time cases and current canonicalization issues are recorded limitations, not intended feature grants.
Validation interval/offline grace are not constructor options. Configure the product on KDPika, then verify what the signed server_time/next_validation_at/offline_grace_until actually say. Offline grace is based on server_time, not added after next_validation_at. Do not calculate a second extension or treat a failed check as permission.
Read the full offline guide and test vectors. Known Python separator/exponent, JavaScript integral-float and C# cached-Unicode compatibility failures remain unchanged. A successful narrow local lifecycle is not universal canonicalization conformance.
Threading, asynchronous work and lifetime
Async methods return Promises, but filesystem operations are synchronous and there is no shared-instance/thread/process locking. Serialize lifecycle operations and do not race activation/deactivation across workers. Node worker threads and multiple Electron instances must not write one namespace concurrently without your own coordination.
Application and framework integration
Electron: construct the client only in the main process, keep contextIsolation enabled, expose narrow application actions through preload/IPC, and never return tokens/raw cache/arbitrary SDK methods to the renderer. The package is ESM only; CommonJS callers can use awaited dynamic import in an async entry point, not assume require() is an official CJS export. A web frontend must use its own authenticated backend and must not receive lmp_ developer keys.
Packaging and distribution
The complete Electron app uses pinned Electron44.9.0/electron-builder26.15.3 and includes the exact local SDK directory. npm ci restores the locked project; npm start runs the source window and npm run package builds its directory artifact. Both source and unsigned macOS packaged windows pass real Sandbox lifecycle/offline/refusal tests. Keep storageDir outside ASAR/installation paths. Code signing, notarization, other native OS packages and real safeStorage backends remain separate deployment tests; this SDK does not automatically encrypt its token files.
Upgrade and uninstall
Pin the current source checksum and runtime/dependency lock. Before changing a version, compare constructor/public members, storage namespace, canonical bytes, time policy and errors. Run activation/cache reload/offline/explicit refusal/deactivation on a separate Sandbox installation. No historical version or compatibility window is invented by this guide.
Deactivate while online before intentionally removing a deployed installation. Removing the package alone does not release a server seat or erase your application storage. Retaining storage may preserve identity for a later reinstall; deleting it can create another installation. Follow your application privacy/retention policy rather than recursively deleting a broad user directory.
The language-specific removal command applies only if the package/reference was installed into that application project:
npm uninstall @kdpika/license-sdk
For a direct copied PHP source directory, removing a Composer root package is not the same as composer remove in a consuming application. Remove only the exact unused reference/files after backing up required application state; no broad deletion command is prescribed.
Expected result
Your application handles explicit refusal without enabling features, preserves the installation identity, and deactivates deliberately when releasing a seat. Check the real signed result, not only a transport status.
Common mistakes
- Using activated/token presence or a feature helper as a validity proof.
- Putting /api/v1 in the SDK site-origin constructor or omitting SANDBOX in a test.
- Deleting identity/cache and assuming a server seat or cumulative activation was reset.
- Suppressing a signature mismatch instead of testing the actual release limitation.
Security notes
Keep credentials off renderer/browser surfaces, retain TLS/signature verification, and use a verified valid result for the specific access action. A successful deactivate/status operation never grants a feature. Use private storage and safe correlation metadata only.
Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| NOT_ACTIVATED | No stored credential in this namespace | Activate a Sandbox installation; keep origin/product/environment/storage stable |
| INVALID_LICENSE | Wrong key/email/App ID/environment | Correct the input; do not retry unchanged requests blindly |
| SIGNATURE_INVALID or cache rejection | Tampering, trust mismatch or current canonicalization limitation | Fail closed and run real response/cache/vector checks |
| State disappeared after update | Application name/origin/storage namespace changed | Restore the intended stable namespace; reconcile any extra seat |
| Network failure at startup | Activation requires network or cache is unavailable/invalid | Reconnect or show a clear retry/support path without unlocking |
Next steps
Review the Sandbox quickstart, signed responses and error handling.