Use the existing API v1 directly from a trusted Node service, CLI or Electron main process. This complete REST example is not an additional official SDK or a browser licensing library.
What you will learn
- Activate, check, validate and deactivate one stable Sandbox installation.
- Authenticate the exact signed JSON bytes without losing integral-float spellings.
- Enforce binding, status, start, expiry, grace and rollback policy before features.
- Keep tokens private and distinguish transport failure from explicit rejection.
Prerequisites
- Complete the Sandbox quickstart.
- Use Node 22+, TypeScript 7.0.2 and @types/node 22.19.7. Actual execution is macOS/Node 22.22.2, not native Windows/Linux certification.
- Obtain a Sandbox Public App ID, a Sandbox license and its matching email when using email-bound activation. Never put an lmp_ developer key in this application.
- Obtain the actual public-key document from GET /api/v1/public-key over your independently trusted HTTPS origin. Pin that document through your application configuration or controlled release process. Do not accept a public key supplied alongside an untrusted license envelope.
- Choose an absolute private state directory outside public web roots and the application installation/ASAR directory.
Project setup and pinned dependencies
Preserve this layout. Both JavaScript files are the complete, already published verification sources, not replacements for the official SDK canonicalizer.
samples/
sdks/
typescript/
client.ts
package.json
package-lock.json
security/
javascript/
canonical.mjs
verify.mjs
{
"name": "kdpika-typescript-rest-example",
"private": true,
"version": "1.0.0",
"type": "module",
"engines": {"node": ">=22"},
"devDependencies": {"@types/node": "22.19.7", "typescript": "7.0.2"}
}
<!-- P7-PROJECT-PACKAGE:START -->
The complete project archive contains every client/verifier source and the exact build/dependency metadata needed by this language. Extract it with its original directory layout. Dependencies/locks are included where this runtime uses them; no hidden helper or test-only clock is required.
kdpika-typescript-rest-example-1.0.0.zip · 10688 bytes · version 1.0.0
SHA-256: b8411aa54bfeca668d65473003b5680d125a7528a119708c579c96648e855cb8
<!-- P7-PROJECT-PACKAGE:END -->
From the TypeScript project directory, install the pinned development tools. No runtime npm dependency is needed by the client. Node/compiler installation is on your development machine, not KDPika's shared PHP hosting.
npm ci --ignore-scripts --no-audit --no-fund
npx --no-install tsc --strict --allowJs --checkJs false --target es2022 --module nodenext --moduleResolution nodenext --types node --outDir ./build client.ts
Expected: strict compilation with no diagnostics. The executable is build/sdks/typescript/client.js; keep it with build/security/javascript and the project module metadata. The documentation harness verifies this exact clean standalone layout, lockfile installation and compilation, then executes that compiled client against the isolated Sandbox. No downloadable TypeScript archive is claimed before the complete phase publication gate.
Complete session client
// Complete Sandbox REST session client, not an additional official SDK.
// Node22+, TypeScript7.0.2, @types/node22.19.7; no runtime npm dependencies.
// Keep the adjacent published security/javascript modules in their layout.
import * as fs from "node:fs";
import * as path from "node:path";
import {createHash, randomBytes, randomUUID} from "node:crypto";
import {verifyEnvelope} from "../../security/javascript/verify.mjs";
import {parseRaw, canonical} from "../../security/javascript/canonical.mjs";
type ObjectValue = Record<string, unknown>;
type State = {token: string; envelope: string | null; checkpoint: string};
type Decision = {valid: boolean; source: string; signature_valid: boolean; error_code: string | null;
request_id: string | null; pdf_export_allowed: boolean; seat_count: number | null; message: string};
class Refusal extends Error {
constructor(readonly code: string, readonly requestId: string | null = null) {super(code);}
}
class Unreachable extends Error {}
const decode = (bytes: Uint8Array): unknown => parseRaw(bytes);
const object = (value: unknown): ObjectValue => {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Refusal("INVALID_RESPONSE");
return value as ObjectValue;
};
const time = (value: unknown): number => {
if (typeof value !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(value)) throw new Refusal("INVALID_TIME");
const result = Date.parse(value);
if (!Number.isFinite(result)) throw new Refusal("INVALID_TIME");
return result;
};
const friendly = (code: string): string => {
if (code === "INVALID_LICENSE") return "Check your license key and the email used for this license.";
if (code === "NOT_ACTIVATED") return "Activate this installation to continue.";
if (["NETWORK_ERROR", "OFFLINE_GRACE_EXPIRED"].includes(code)) return "Connect to the internet and try again.";
if (code === "CLOCK_ROLLBACK_DETECTED") return "Check your device date and time, then try again.";
return "Your license could not be confirmed. Please contact your software provider.";
};
const denied = (code: string, source: string, requestId: string | null = null): Decision => ({valid: false, source,
signature_valid: false, error_code: code, request_id: requestId, pdf_export_allowed: false, seat_count: null, message: friendly(code)});
class Session {
readonly directory: string;
readonly installation: string;
private readonly trusted: Buffer;
private readonly api: string;
private readonly app: string;
private readonly lock: string;
private readonly lockHandle: number;
constructor() {
this.app = process.env.KDP_APP_ID ?? "";
if (!/^[A-Za-z0-9_-]{3,160}$/.test(this.app)) throw new Error("Set KDP_APP_ID.");
const rawBase = process.env.KDP_BASE_URL ?? "https://licensing.kdpika.com/api/v1";
const base = new URL(rawBase);
const local = base.protocol === "http:" && ["127.0.0.1", "localhost"].includes(base.hostname) && process.env.KDP_ALLOW_LOOPBACK === "1";
if ((base.protocol !== "https:" && !local) || base.username || base.password || base.search || base.hash || base.pathname.replace(/\/$/, "") !== "/api/v1") throw new Error("Use the HTTPS API URL; only explicit loopback HTTP is allowed.");
this.api = base.origin + "/api/v1";
const root = process.env.KDP_STATE_DIR ?? "";
const trustedFile = process.env.KDP_TRUSTED_KEY_FILE ?? "";
if (!path.isAbsolute(root) || !path.isAbsolute(trustedFile)) throw new Error("Set absolute private state and independently trusted key paths.");
this.trusted = this.readFile(trustedFile);
const key = object(decode(this.trusted));
if (key.algorithm !== "Ed25519" || key.encoding !== "base64url") throw new Error("Use the actual trusted public-key document.");
const namespace = createHash("sha256").update(this.api + "|" + this.app + "|SANDBOX|typescript-rest-v1").digest("hex").slice(0, 20);
fs.mkdirSync(root, {recursive: true, mode: 0o700});
if (fs.lstatSync(root).isSymbolicLink()) throw new Error("State must not be a symlink.");
this.directory = path.join(root, namespace);
fs.mkdirSync(this.directory, {mode: 0o700, recursive: true});
if (fs.lstatSync(this.directory).isSymbolicLink()) throw new Error("Namespace must not be a symlink.");
fs.chmodSync(this.directory, 0o700);
this.lock = path.join(this.directory, ".lock");
this.lockHandle = fs.openSync(this.lock, "wx", 0o600);
try {
const identity = path.join(this.directory, "installation.json");
if (!fs.existsSync(identity)) this.write("installation.json", {installation_id: "lmp-install-" + randomUUID()});
const value = object(JSON.parse(this.readFile(identity).toString("utf8"))).installation_id;
if (typeof value !== "string" || !/^lmp-install-[0-9a-f-]{36}$/.test(value)) throw new Error("Installation identity is invalid; do not reset automatically.");
this.installation = value;
} catch (error) {this.close(); throw error;}
}
close(): void {fs.closeSync(this.lockHandle); fs.unlinkSync(this.lock);}
private readFile(file: string): Buffer {
const stat = fs.lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) throw new Error("Unsafe state/key file.");
return fs.readFileSync(file);
}
private write(name: string, value: unknown): void {
const target = path.join(this.directory, name), temporary = target + "." + randomBytes(12).toString("hex");
fs.writeFileSync(temporary, JSON.stringify(value), {flag: "wx", mode: 0o600});
fs.renameSync(temporary, target);
}
private state(): State | null {
const file = path.join(this.directory, "session.json");
if (!fs.existsSync(file)) return null;
const value = object(JSON.parse(this.readFile(file).toString("utf8")));
if (typeof value.token !== "string" || !/^act_[A-Za-z0-9_-]+$/.test(value.token) ||
!(value.envelope === null || typeof value.envelope === "string") || typeof value.checkpoint !== "string") throw new Error("Stored session is invalid.");
return value as State;
}
private clear(): void {
const file = path.join(this.directory, "session.json");
if (fs.existsSync(file)) {this.readFile(file); fs.unlinkSync(file);}
}
private async request(endpoint: string, body: ObjectValue, token?: string): Promise<{body: ObjectValue; requestId: string | null}> {
const headers: Record<string, string> = {Accept: "application/json", "Content-Type": "application/json", "User-Agent": "KDPika-REST-TypeScript-example/1.0"};
if (token) {headers.Authorization = "Bearer " + token; headers["X-Installation-ID"] = this.installation;}
else {headers["X-App-ID"] = this.app; headers["X-Environment"] = "SANDBOX";}
let response: Response, raw: Buffer;
try {
response = await fetch(this.api + endpoint, {method: "POST", headers, body: JSON.stringify(body), redirect: "error", signal: AbortSignal.timeout(15000)});
const reader = response.body?.getReader(); if (!reader) throw new Refusal("INVALID_RESPONSE");
const chunks: Uint8Array[] = []; let size = 0;
for (;;) {
const next = await reader.read(); if (next.done) break;
size += next.value.length;
if (size > 1048576) {await reader.cancel(); throw new Refusal("INVALID_RESPONSE");}
chunks.push(next.value);
}
raw = Buffer.concat(chunks);
} catch (error) {if (error instanceof Refusal) throw error; throw new Unreachable();}
let value: ObjectValue;
try {value = object(decode(raw));} catch {throw new Refusal("INVALID_RESPONSE");}
const requestId = typeof value.request_id === "string" && /^req_[A-Za-z0-9_-]{1,120}$/.test(value.request_id) ? value.request_id : null;
if (!response.ok || value.success !== true) {
const error = value.error && typeof value.error === "object" ? object(value.error) : {};
const code = typeof error.code === "string" && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : "HTTP_ERROR";
throw new Refusal(code, requestId);
}
return {body: value, requestId};
}
private verify(envelope: string, source: string, checkpoint: string | null, requestId: string | null): Decision {
if (!verifyEnvelope(this.trusted, Buffer.from(envelope))) throw new Refusal("SIGNATURE_INVALID", requestId);
// Only after byte-level verification, decode a semantic view for policy.
const payload = object(object(JSON.parse(envelope)).payload), now = Date.now();
if (payload.product_id !== this.app || payload.environment !== "sandbox" ||
object(payload.device).installation_hash !== createHash("sha256").update(this.installation).digest("hex")) throw new Refusal("BINDING_MISMATCH", requestId);
const server = time(payload.server_time), next = time(payload.next_validation_at), grace = time(payload.offline_grace_until);
if (next < server || grace < server) throw new Refusal("INVALID_TIME", requestId);
const previous = checkpoint === null ? server : Math.max(server, time(checkpoint));
if (now + 300000 < previous) throw new Refusal("CLOCK_ROLLBACK_DETECTED", requestId);
if (payload.status !== "active") throw new Refusal("LICENSE_NOT_ACTIVE", requestId);
if (payload.starts_at !== null && payload.starts_at !== undefined && now < time(payload.starts_at)) throw new Refusal("LICENSE_NOT_STARTED", requestId);
if (payload.expires_at !== null && now >= time(payload.expires_at)) throw new Refusal("LICENSE_EXPIRED", requestId);
if (source !== "online" && now > grace) throw new Refusal("OFFLINE_GRACE_EXPIRED", requestId);
const features = object(payload.features), seat = features.seat_count;
return {valid: true, source, signature_valid: true, error_code: null, request_id: requestId,
pdf_export_allowed: features.pdf_export === true, seat_count: typeof seat === "number" && Number.isSafeInteger(seat) && seat >= 0 ? seat : null,
message: "Your license is ready."};
}
private accepted(state: State, source: string, requestId: string | null): Decision {
if (!state.envelope) throw new Refusal("OFFLINE_CACHE_MISSING");
const result = this.verify(state.envelope, source, state.checkpoint, requestId);
this.write("session.json", {...state, checkpoint: new Date().toISOString()});
return result;
}
async run(action: string): Promise<Decision | ObjectValue> {
let state = this.state();
if (action === "status") return {activated: state !== null, installation_hash: createHash("sha256").update(this.installation).digest("hex"), storage_security: "permission-protected-file"};
if (action === "activate") {
const license = process.env.KDP_LICENSE_KEY ?? "", email = process.env.KDP_CUSTOMER_EMAIL ?? "";
if (!license.startsWith("TEST") || /[\r\n]/.test(license + email)) throw new Error("Use a Sandbox test key and valid input.");
// Never replace an existing installation token implicitly or retry a
// mutation automatically. Reconcile an uncertain activation first.
if (state) throw new Refusal("ALREADY_ACTIVATED");
const response = await this.request("/licenses/activate", {license_key: license, device_id: this.installation,
device_name: "TypeScript REST example", platform: process.platform, app_version: "1.0.0", ...(email ? {email} : {})});
const token = response.body.activation_token;
if (typeof token !== "string" || !/^act_[A-Za-z0-9_-]+$/.test(token)) throw new Refusal("INVALID_RESPONSE", response.requestId);
try {
const envelope = canonical(object(response.body.license));
const result = this.verify(envelope, "online", null, response.requestId);
this.write("session.json", {token, envelope, checkpoint: new Date().toISOString()});
return result;
} catch (error) {
try {await this.request("/licenses/deactivate", {}, token);} catch { /* Reconcile uncertain release with the developer. */ }
throw error;
}
}
if (!state) throw new Refusal("NOT_ACTIVATED");
if (action === "deactivate") {
const response = await this.request("/licenses/deactivate", {}, state.token);
if (response.body.deactivated !== true) throw new Refusal("INVALID_RESPONSE", response.requestId);
this.clear();
return {valid: true, source: "online", signature_valid: false, error_code: null, request_id: response.requestId,
pdf_export_allowed: false, seat_count: null, message: "This installation has been deactivated."};
}
if (action === "check" && state.envelope) {
try {
const checked = this.verify(state.envelope, "cache", state.checkpoint, null);
const payload = object(object(JSON.parse(state.envelope)).payload);
if (Date.now() < time(payload.next_validation_at)) {this.write("session.json", {...state, checkpoint: new Date().toISOString()}); return checked;}
} catch {state = {...state, envelope: null}; this.write("session.json", state);}
}
try {
const response = await this.request("/licenses/validate", {}, state.token);
const envelope = canonical(object(response.body.license));
const result = this.verify(envelope, "online", state.checkpoint, response.requestId);
this.write("session.json", {...state, envelope, checkpoint: new Date().toISOString()}); return result;
} catch (error) {
if (error instanceof Unreachable) return this.accepted(state, "offline", null);
if (error instanceof Refusal) {
if (["INVALID_TOKEN", "DEVICE_MISMATCH", "LICENSE_REVOKED", "LICENSE_EXPIRED", "LICENSE_CANCELLED"].includes(error.code)) this.clear();
else this.write("session.json", {...state, envelope: null});
}
throw error;
}
}
}
async function main(): Promise<void> {
const action = process.argv[2] ?? "";
if (process.argv.length !== 3 || !["status", "activate", "check", "validate", "deactivate"].includes(action)) throw new Error("Choose status, activate, check, validate or deactivate.");
const session = new Session();
try {
try {
const result = await session.run(action); console.log(JSON.stringify(result));
process.exitCode = "valid" in result && result.valid !== true ? 1 : 0;
} catch (error) {
if (error instanceof Refusal || error instanceof Unreachable) {
console.log(JSON.stringify(denied(error instanceof Refusal ? error.code : "NETWORK_ERROR", "local", error instanceof Refusal ? error.requestId : null)));
process.exitCode = 1;
} else throw error;
}
} finally {session.close();}
}
main().catch(() => {console.error("Check your application settings and private storage. Please contact your software provider if the problem continues."); process.exitCode = 2;});
Download the complete, already published verification source archive and copy security/javascript/canonical.mjs and security/javascript/verify.mjs into the layout above without modification. Both files are also displayed in full on the JavaScript verification page. This client imports those exact tested sources; it does not omit or replace verification logic.
The companion modules retain numeric tokens, reject duplicate properties, malformed UTF-8/Unicode, trailing content, excessive depth and oversized input. Signature verification independently derives the key ID and checks the key/envelope algorithm before Ed25519 verification.
Configure the application
Set these values through trusted application configuration or your private development environment. Do not print the environment in logs or embed a customer's license in distributed source.
| Variable | Required | Meaning |
|---|---|---|
| KDP_APP_ID | Yes | Public App ID, not internal product ID or developer API key |
| KDP_BASE_URL | No | API URL, default https://licensing.kdpika.com/api/v1; unlike official SDK constructors, this REST example includes /api/v1 |
| KDP_STATE_DIR | Yes | Absolute private application state root; namespaces include API URL, App ID, SANDBOX and this client's format |
| KDP_TRUSTED_KEY_FILE | Yes | Absolute path to the independently trusted public-key JSON document |
| KDP_LICENSE_KEY | Activation only | Synthetic Sandbox key starting TEST for this example's safety gate |
| KDP_CUSTOMER_EMAIL | Optional | Matching license email; key-only activation is an actual API mode, not an email policy enforced by the example |
| KDP_ALLOW_LOOPBACK | Local testing only | Set 1 to allow explicit localhost/127.0.0.1 HTTP; never an arbitrary non-TLS host |
Environment is deliberately fixed to SANDBOX. Live keys, URL credentials, query strings, fragments and nonlocal HTTP are blocked. Redirects are not followed, and the request timeout is 15 seconds. Requests and response bodies are not logged.
Run the lifecycle
Run the compiled client with exactly one action: status, activate, check, validate or deactivate. Keep the same state root and App ID between invocations.
- Run status. Expected: activated=false and an installation hash, never the stored token. This action is not a validity check.
- Set the Sandbox key and matching email, then run activate. Expected: a signed accepted decision with signature_valid=true or a friendly refusal. An existing token is never replaced implicitly.
- Run check. Expected: eligible verified cache before next_validation_at, otherwise an online validation attempt. No valid result means no feature access.
- Run validate. Expected: an online attempt; a transport failure can use only a still-valid signed offline cache. This is not an unconditional network-only guarantee.
- Run deactivate. Expected: server-confirmed seat release and removal of the local session. Its valid=true describes the release operation; signature_valid=false and pdf_export_allowed=false ensure it cannot be mistaken for access.
- Run status again. Expected: activated=false and the same installation hash. Deactivation does not reset cumulative activation_count.
Exit codes are 0 for an accepted operation/status, 1 for a licensing or connectivity refusal, and 2 for invalid configuration, unsafe local state or an unexpected failure. Output contains decisions and a safe request ID, not raw signed customer data, keys or tokens.
Contract and feature policy
The client calls the actual activate, validate and deactivate endpoints. Activation sends X-App-ID and X-Environment: SANDBOX; validation sends Bearer act_ and X-Installation-ID; the installation token is never an lmp_ developer management key. See authentication.
Signature verification uses a lossless parsed representation to recover the signed envelope without changing numeric tokens. Only after authentication does the client decode a semantic view for policy. It requires the actual App ID, environment=sandbox, SHA-256 installation binding and active status. Start, expiry and signed validation/grace dates are checked separately.
pdf_export is enabled only when a fully accepted result contains the boolean true. seat_count is exposed only as a nonnegative safe integer; absent, fractional, unsafe or nonnumeric limits remain null. These are example feature names: configure the features your own application actually uses. Constructing an object with valid=true is never a verification step.
Offline grace and clock checks
The signed server_time, next_validation_at and offline_grace_until are mandatory valid timestamps in this example. The validation/grace dates cannot precede server_time. This strict client policy is deliberately fail-closed; it is not a new API response contract or a change to existing SDK behavior.
Before next_validation_at, check may use eligible verified cache. Afterwards, a transport failure may use a still-valid signed cache. An HTTP rejection, malformed success, signature failure or binding failure is not permission to enter offline mode. Explicit refusal removes the cached envelope; selected invalid-token/device/revocation/expiry/cancellation codes also remove the token.
The current instant may not be more than 300 seconds behind the later signed server time or stored checkpoint. Expiry rejects at the exact expiry instant. Grace accepts its exact boundary and rejects after it. Grace is not extended by adding it to next_validation_at. The test harness evaluates the actual signed boundaries through a test-only clock preload; the distributed client exposes no clock override variable.
No offline scheme prevents an attacker who controls and patches the local application or restores a full machine snapshot. Preserve the signature/time checks and explain connectivity requirements to users without promising absolute anti-tamper protection.
Storage, concurrency and token protection
The sample creates a persistent random installation ID, a private namespace and an atomic permission-protected session file. The session contains the token, raw signed envelope and last accepted checkpoint. It rejects symlink files, oversized state/key files and invalid identity/session formats. It never resets an invalid installation identity automatically.
An exclusive lock serializes operations for the namespace. If a process crashes, reconcile the process state before removing its exact stale lock; do not run multiple writers or automatically discard a lock that may still be in use. Atomic writes and Unix permission modes are not encrypted vault storage or protection against a malicious same-user process. Windows ACLs must be reviewed on the real target system. See secure token storage before production desktop distribution.
Do not share one namespace across unrelated buyers, tenants, products or environments. Store state outside a public server directory. A backend route wrapping this client still needs your application's authentication, tenant authorization and CSRF protection. An Electron renderer must not receive the token, raw cache, private paths or an arbitrary SDK/REST invocation channel.
Errors and uncertain operations
The API's error.code and request_id are handled through bounded safe strings. Unknown errors fail closed. See the complete error table for server meanings. Example-local errors such as ALREADY_ACTIVATED, BINDING_MISMATCH, INVALID_TIME and SIGNATURE_INVALID describe this client policy; they are not invented server emissions.
No mutation is automatically retried. After a lost activation response, reconcile whether the server already consumed an activation before retrying. If a token was returned but signature/policy acceptance fails, the example attempts a best-effort release of that exact new token; a failed release still needs reconciliation by the developer. A missing/incomplete response cannot guarantee that no server mutation happened.
Keep friendly messages separate from support diagnostics. Record only time, action, application version, safe error code and safe request ID. Do not log Authorization headers, environment dumps, license keys, tokens, raw exceptions, private paths or customer-bearing payloads.
Packaging and integration notes
Ship the compiled client with the unchanged verification modules and a controlled trusted-key configuration. Do not include a test-only clock preload, populated state directory, customer license key or developer API key. Re-run real signed response, cache reload, explicit refusal, offline and deactivation tests after bundling.
This sample is a trusted Node/CLI/Electron-main integration. It is not suitable for ordinary browser storage or unrestricted renderer access. Native Windows/Linux execution, encrypted vault adapters and a standalone bundled binary are not certified by these macOS source-client checks. API v1 is independent of this sample's version label and the four official SDK package versions.
Verification recorded so far
Strict compilation uses the pinned TypeScript and Node type versions. The exact program runs against a fresh isolated local Sandbox with real signed responses. Tests cover matching/wrong email, stable identity, cache with zero HTTP, online validation, injected transport loss, modified cache, rollback, explicit HTTP refusal, modified server signature, redirect rejection, deactivation and unsafe setup inputs. The real signed grace and expiry boundaries are checked without editing the signed data.
All23 published signature vectors also pass through the TypeScript verification modules. The complete project builds from a fresh exact source archive with its pinned lockfile. Signature-vector results establish authenticity and canonicalization, not present license validity. The separate lifecycle tests enforce binding, status, clock and feature policy. Native target-OS packaging and vault behavior need their own release tests.
Expected result
Your application preserves one installation identity, authenticates signed state and applies policy before any feature access. Explicit refusals remain refusals, and a successful release never unlocks the application.
Common mistakes
- Copying this REST API base URL into an official SDK constructor, which expects only the site origin.
- Using JSON.parse/JSON.stringify as the signed-byte canonicalizer and losing 19.0 to 19.
- Trusting a public key received from an untrusted envelope instead of your controlled key configuration.
- Treating token presence, HTTP 200, a true feature helper or deactivation success as current permission.
- Deleting identity/state to fix a refusal and unknowingly consuming another activation.
Security notes
This complete example uses the existing server contract without changing licensing behavior. Keep HTTPS, trusted-key verification, product/environment/installation binding, expiry/grace and clock checks. Test-mode guards reduce mistakes; they do not replace the platform's credential and environment validation. No local constructed or synthetic test result authorizes a real customer installation.
Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| Configuration exit 2 | Missing absolute path, unsafe URL, invalid file or busy/stale lock | Correct trusted configuration; inspect the exact private namespace without exposing it to users |
| INVALID_LICENSE | Key, email, App ID or environment mismatch | Use the real Sandbox values together; do not retry unchanged input repeatedly |
| SIGNATURE_INVALID | Modified data, wrong trusted key or incompatible bytes | Deny access; compare the published vectors and controlled key version |
| OFFLINE_CACHE_MISSING | No accepted signed cache remains | Reconnect and validate; do not manufacture an offline result |
| CLOCK_ROLLBACK_DETECTED | Device clock is behind trusted time | Correct the system clock and reconnect |
| Release failed | Network/server refusal or uncertain response | Keep the installation state and reconcile with the software provider |
Next steps
Review signed responses, offline mode and the API conventions before adapting this Sandbox client.