@licensr/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ import { jwtVerify, createRemoteJWKSet, customFetch } from 'jose';
2
+
3
+ // src/events.ts
4
+ var LicensrEventEmitter = class {
5
+ constructor() {
6
+ this.listeners = /* @__PURE__ */ new Map();
7
+ }
8
+ /** Returns an unsubscribe function. */
9
+ on(event, listener) {
10
+ let set = this.listeners.get(event);
11
+ if (!set) {
12
+ set = /* @__PURE__ */ new Set();
13
+ this.listeners.set(event, set);
14
+ }
15
+ set.add(listener);
16
+ return () => this.off(event, listener);
17
+ }
18
+ off(event, listener) {
19
+ this.listeners.get(event)?.delete(listener);
20
+ }
21
+ emit(event, payload) {
22
+ const set = this.listeners.get(event);
23
+ if (!set) return;
24
+ for (const listener of set) listener(payload);
25
+ }
26
+ };
27
+
28
+ // src/errors.ts
29
+ var LicensrApiError = class extends Error {
30
+ constructor(status, code, message, retryAfterSeconds = null) {
31
+ super(message);
32
+ this.name = "LicensrApiError";
33
+ this.status = status;
34
+ this.code = code;
35
+ this.retryAfterSeconds = retryAfterSeconds;
36
+ }
37
+ };
38
+ var LicensrNetworkError = class extends Error {
39
+ constructor(message, cause) {
40
+ super(message);
41
+ this.name = "LicensrNetworkError";
42
+ this.cause = cause;
43
+ }
44
+ };
45
+ var LicensrTokenVerificationError = class extends Error {
46
+ constructor(message) {
47
+ super(message);
48
+ this.name = "LicensrTokenVerificationError";
49
+ }
50
+ };
51
+
52
+ // src/http.ts
53
+ var DEVICE_ID_HEADER = "X-Licensr-Device-Id";
54
+ var DEFAULT_RETRY = {
55
+ maxRetries: 2,
56
+ baseDelayMs: 300,
57
+ maxDelayMs: 5e3
58
+ };
59
+ function buildUrl(baseUrl, path, query) {
60
+ const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
61
+ if (query) {
62
+ for (const [key, value] of Object.entries(query)) {
63
+ if (value !== void 0) url.searchParams.set(key, value);
64
+ }
65
+ }
66
+ return url.toString();
67
+ }
68
+ function sleep(ms) {
69
+ return new Promise((resolve) => setTimeout(resolve, ms));
70
+ }
71
+ function jitterDelayMs(attempt, baseDelayMs, maxDelayMs) {
72
+ const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
73
+ return Math.random() * cap;
74
+ }
75
+ function parseRetryAfterSeconds(header) {
76
+ if (!header) return null;
77
+ const seconds = Number(header);
78
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
79
+ }
80
+ async function parseErrorDetail(response) {
81
+ let json;
82
+ try {
83
+ json = await response.json();
84
+ } catch {
85
+ return { code: "unknown_error", message: response.statusText || `HTTP ${response.status}` };
86
+ }
87
+ const detail = json?.detail;
88
+ if (detail && typeof detail === "object" && !Array.isArray(detail) && "error" in detail) {
89
+ const { error, message } = detail;
90
+ return {
91
+ code: typeof error === "string" ? error : "unknown_error",
92
+ message: typeof message === "string" ? message : response.statusText
93
+ };
94
+ }
95
+ if (Array.isArray(detail) && detail.length > 0) {
96
+ const first = detail[0];
97
+ return {
98
+ code: "validation_error",
99
+ message: typeof first?.msg === "string" ? first.msg : "Request validation failed"
100
+ };
101
+ }
102
+ return { code: "unknown_error", message: response.statusText || `HTTP ${response.status}` };
103
+ }
104
+ async function requestJson(config, spec) {
105
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
106
+ if (!fetchImpl) {
107
+ throw new LicensrNetworkError(
108
+ "No fetch implementation available. Pass `fetchImpl` explicitly on Node < 18.",
109
+ void 0
110
+ );
111
+ }
112
+ const retry = { ...DEFAULT_RETRY, ...config.retry };
113
+ const url = buildUrl(config.baseUrl, spec.path, spec.query);
114
+ for (let attempt = 0; ; attempt += 1) {
115
+ const headers = new Headers({ Authorization: `Bearer ${config.apiKey}` });
116
+ if (spec.body !== void 0) headers.set("Content-Type", "application/json");
117
+ if (config.deviceId) headers.set(DEVICE_ID_HEADER, config.deviceId);
118
+ if (config.origin) {
119
+ try {
120
+ headers.set("Origin", config.origin);
121
+ } catch {
122
+ }
123
+ }
124
+ const controller = config.timeoutMs !== void 0 ? new AbortController() : void 0;
125
+ const timeoutHandle = controller && config.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), config.timeoutMs) : void 0;
126
+ let response;
127
+ try {
128
+ response = await fetchImpl(url, {
129
+ method: spec.method,
130
+ headers,
131
+ body: spec.body !== void 0 ? JSON.stringify(spec.body) : void 0,
132
+ signal: controller?.signal
133
+ });
134
+ } catch (cause) {
135
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
136
+ if (attempt < retry.maxRetries) {
137
+ const delayMs = jitterDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs);
138
+ spec.onRetry?.(attempt + 1, delayMs);
139
+ await sleep(delayMs);
140
+ continue;
141
+ }
142
+ throw new LicensrNetworkError(`Request to ${spec.path} failed after ${attempt + 1} attempt(s)`, cause);
143
+ }
144
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
145
+ if (response.ok) {
146
+ if (response.status === 204) return void 0;
147
+ return await response.json();
148
+ }
149
+ const retriable = response.status === 429 || response.status >= 500;
150
+ if (retriable && attempt < retry.maxRetries) {
151
+ const retryAfterSeconds = parseRetryAfterSeconds(response.headers.get("Retry-After"));
152
+ const delayMs = retryAfterSeconds !== null ? retryAfterSeconds * 1e3 : jitterDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs);
153
+ spec.onRetry?.(attempt + 1, delayMs);
154
+ await sleep(delayMs);
155
+ continue;
156
+ }
157
+ const { code, message } = await parseErrorDetail(response);
158
+ throw new LicensrApiError(
159
+ response.status,
160
+ code,
161
+ message,
162
+ parseRetryAfterSeconds(response.headers.get("Retry-After"))
163
+ );
164
+ }
165
+ }
166
+
167
+ // src/wire.ts
168
+ function fromValidateWire(wire) {
169
+ return {
170
+ valid: wire.valid,
171
+ status: wire.status,
172
+ planId: wire.plan_id,
173
+ planName: wire.plan_name,
174
+ activationMode: wire.activation_mode,
175
+ maxSeats: wire.max_seats,
176
+ maxDomains: wire.max_domains,
177
+ seatsInUse: wire.seats_in_use,
178
+ expiresAt: wire.expires_at,
179
+ entitlement: wire.entitlement,
180
+ fallback: wire.fallback,
181
+ featureFlags: wire.feature_flags
182
+ };
183
+ }
184
+ function fromTokenWire(wire) {
185
+ return {
186
+ token: wire.token,
187
+ algorithm: wire.algorithm,
188
+ kid: wire.kid,
189
+ issuedAt: wire.issued_at,
190
+ expiresAt: wire.expires_at,
191
+ jwksUrl: wire.jwks_url
192
+ };
193
+ }
194
+ function fromActivateWire(wire) {
195
+ return {
196
+ activationId: wire.activation_id,
197
+ activationType: wire.activation_type,
198
+ identifier: wire.identifier,
199
+ featureFlags: wire.feature_flags
200
+ };
201
+ }
202
+ function fromDeactivateWire(wire) {
203
+ return { deactivated: wire.deactivated };
204
+ }
205
+ function fromActivationListItemWire(wire) {
206
+ return {
207
+ activationId: wire.activation_id,
208
+ activationType: wire.activation_type,
209
+ identifier: wire.identifier,
210
+ label: wire.label,
211
+ firstSeenAt: wire.first_seen_at,
212
+ lastSeenAt: wire.last_seen_at
213
+ };
214
+ }
215
+ function fromActivationsWire(wire) {
216
+ return {
217
+ licenseId: wire.license_id,
218
+ activations: wire.activations.map(fromActivationListItemWire)
219
+ };
220
+ }
221
+ function fromCheckoutWire(wire) {
222
+ return { checkoutUrl: wire.checkout_url };
223
+ }
224
+
225
+ // src/client.ts
226
+ var LicensrClient = class {
227
+ constructor(config) {
228
+ this.events = new LicensrEventEmitter();
229
+ this.pluginSlug = config.pluginSlug;
230
+ this.http = {
231
+ baseUrl: config.baseUrl ?? "https://api.licensr.app",
232
+ apiKey: config.apiKey,
233
+ origin: config.origin,
234
+ deviceId: config.deviceId,
235
+ fetchImpl: config.fetchImpl,
236
+ timeoutMs: config.timeoutMs,
237
+ retry: config.retry
238
+ };
239
+ }
240
+ /** Check whether a license is valid for this plugin, without minting an offline token. */
241
+ async validate(req) {
242
+ return this.run("validate", async () => {
243
+ const wire = await requestJson(this.http, {
244
+ method: "POST",
245
+ path: "v1/license/validate",
246
+ body: { license_key: req.licenseKey, plugin_slug: this.pluginSlug },
247
+ onRetry: this.onRetry("validate")
248
+ });
249
+ return fromValidateWire(wire);
250
+ });
251
+ }
252
+ /**
253
+ * Run the same check as {@link validate}, and on success mint a
254
+ * short-lived EdDSA-signed offline token. Verify it with
255
+ * {@link import('./offline.js').verifyOfflineToken} — no network call
256
+ * needed after the first successful fetch of the plugin's JWKS.
257
+ */
258
+ async token(req) {
259
+ return this.run("token", async () => {
260
+ const wire = await requestJson(this.http, {
261
+ method: "POST",
262
+ path: "v1/license/token",
263
+ body: { license_key: req.licenseKey, plugin_slug: this.pluginSlug },
264
+ onRetry: this.onRetry("token")
265
+ });
266
+ return fromTokenWire(wire);
267
+ });
268
+ }
269
+ /** Activate a seat (per-machine HWID) or domain for a license. */
270
+ async activate(req) {
271
+ return this.run("activate", async () => {
272
+ const wire = await requestJson(this.http, {
273
+ method: "POST",
274
+ path: "v1/license/activate",
275
+ body: {
276
+ license_key: req.licenseKey,
277
+ plugin_slug: this.pluginSlug,
278
+ activation_type: req.activationType,
279
+ identifier: req.identifier,
280
+ label: req.label ?? null
281
+ },
282
+ onRetry: this.onRetry("activate")
283
+ });
284
+ return fromActivateWire(wire);
285
+ });
286
+ }
287
+ /** Release a previously created activation, freeing its seat/domain slot. */
288
+ async deactivate(req) {
289
+ return this.run("deactivate", async () => {
290
+ const wire = await requestJson(this.http, {
291
+ method: "POST",
292
+ path: "v1/license/deactivate",
293
+ body: { license_key: req.licenseKey, activation_id: req.activationId },
294
+ onRetry: this.onRetry("deactivate")
295
+ });
296
+ return fromDeactivateWire(wire);
297
+ });
298
+ }
299
+ /** List every current activation for a license (seats/domains in use). */
300
+ async activations(req) {
301
+ return this.run("activations", async () => {
302
+ const wire = await requestJson(this.http, {
303
+ method: "GET",
304
+ path: "v1/license/activations",
305
+ query: { license_key: req.licenseKey, plugin_slug: this.pluginSlug },
306
+ onRetry: this.onRetry("activations")
307
+ });
308
+ return fromActivationsWire(wire);
309
+ });
310
+ }
311
+ /** Start a hosted checkout session for a plan; redirect the user to the returned URL. */
312
+ async checkout(req) {
313
+ return this.run("checkout", async () => {
314
+ const wire = await requestJson(this.http, {
315
+ method: "POST",
316
+ path: "v1/billing/checkout",
317
+ body: {
318
+ plan_id: req.planId,
319
+ customer_email: req.customerEmail,
320
+ success_url: req.successUrl ?? null,
321
+ cancel_url: req.cancelUrl ?? null
322
+ },
323
+ onRetry: this.onRetry("checkout")
324
+ });
325
+ return fromCheckoutWire(wire);
326
+ });
327
+ }
328
+ onRetry(method) {
329
+ return (attempt, delayMs) => this.events.emit("retry", { method, attempt, delayMs });
330
+ }
331
+ async run(method, fn) {
332
+ try {
333
+ const result = await fn();
334
+ this.emitResult(method, result);
335
+ return result;
336
+ } catch (error) {
337
+ this.events.emit("error", { method, error });
338
+ throw error;
339
+ }
340
+ }
341
+ emitResult(method, result) {
342
+ switch (method) {
343
+ case "validate":
344
+ this.events.emit("validated", result);
345
+ break;
346
+ case "activate":
347
+ this.events.emit("activated", result);
348
+ break;
349
+ case "deactivate":
350
+ this.events.emit("deactivated", result);
351
+ break;
352
+ case "token":
353
+ this.events.emit("tokenIssued", result);
354
+ break;
355
+ }
356
+ }
357
+ };
358
+ var jwksSetCache = /* @__PURE__ */ new Map();
359
+ function getJwks(jwksUrl, fetchImpl) {
360
+ let jwks = jwksSetCache.get(jwksUrl);
361
+ if (!jwks) {
362
+ jwks = createRemoteJWKSet(
363
+ new URL(jwksUrl),
364
+ fetchImpl ? { [customFetch]: (url, options) => fetchImpl(url, options) } : void 0
365
+ );
366
+ jwksSetCache.set(jwksUrl, jwks);
367
+ }
368
+ return jwks;
369
+ }
370
+ function clearJwksCache() {
371
+ jwksSetCache.clear();
372
+ }
373
+ async function verifyOfflineToken(token, jwksUrl, options = {}) {
374
+ const jwks = getJwks(jwksUrl, options.fetchImpl);
375
+ try {
376
+ const { payload } = await jwtVerify(token, jwks, {
377
+ algorithms: ["EdDSA"],
378
+ clockTolerance: options.clockToleranceSeconds ?? 60
379
+ });
380
+ return payload;
381
+ } catch (cause) {
382
+ const reason = cause instanceof Error ? cause.message : String(cause);
383
+ throw new LicensrTokenVerificationError(`Offline token verification failed: ${reason}`);
384
+ }
385
+ }
386
+ var InMemoryOfflineTokenStore = class {
387
+ constructor() {
388
+ this.entries = /* @__PURE__ */ new Map();
389
+ }
390
+ get(licenseKey) {
391
+ return this.entries.get(licenseKey) ?? null;
392
+ }
393
+ set(licenseKey, entry) {
394
+ this.entries.set(licenseKey, entry);
395
+ }
396
+ clear(licenseKey) {
397
+ this.entries.delete(licenseKey);
398
+ }
399
+ };
400
+
401
+ export { DEVICE_ID_HEADER, InMemoryOfflineTokenStore, LicensrApiError, LicensrClient, LicensrEventEmitter, LicensrNetworkError, LicensrTokenVerificationError, clearJwksCache, verifyOfflineToken };
402
+ //# sourceMappingURL=index.js.map
403
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/events.ts","../src/errors.ts","../src/http.ts","../src/wire.ts","../src/client.ts","../src/offline.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {ActivateResponse, DeactivateResponse, TokenResponse, ValidateResponse} from './types.js';\n\n/** Lifecycle events emitted by {@link import('./client.js').LicensrClient}. */\nexport interface LicensrEventMap {\n validated: ValidateResponse;\n activated: ActivateResponse;\n deactivated: DeactivateResponse;\n tokenIssued: TokenResponse;\n /** Fired on every retried request, before the retry delay. */\n retry: {method: string; attempt: number; delayMs: number};\n /** Fired whenever a client method throws, right before the error propagates. */\n error: {method: string; error: unknown};\n}\n\ntype Listener<T> = (payload: T) => void;\n\n/**\n * Minimal, dependency-free typed event emitter for {@link LicensrEventMap}\n * — no Node `EventEmitter` import so the SDK stays usable unmodified in\n * browsers.\n */\nexport class LicensrEventEmitter {\n private readonly listeners = new Map<keyof LicensrEventMap, Set<Listener<never>>>();\n\n /** Returns an unsubscribe function. */\n on<K extends keyof LicensrEventMap>(event: K, listener: Listener<LicensrEventMap[K]>): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as Listener<never>);\n return () => this.off(event, listener);\n }\n\n off<K extends keyof LicensrEventMap>(event: K, listener: Listener<LicensrEventMap[K]>): void {\n this.listeners.get(event)?.delete(listener as Listener<never>);\n }\n\n emit<K extends keyof LicensrEventMap>(event: K, payload: LicensrEventMap[K]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const listener of set) (listener as Listener<LicensrEventMap[K]>)(payload);\n }\n}\n","import type {LicensrErrorCode} from './types.js';\n\n/**\n * The API responded with a non-2xx status and a parsed `detail.error` body.\n *\n * `code` is one of the documented error codes in `contract/conformance.yaml`\n * (e.g. `license_inactive`, `origin_not_allowed`, `rate_limit_exceeded`).\n * Branch on `code`, not on `message` — the message is for humans and may\n * change without notice.\n */\nexport class LicensrApiError extends Error {\n readonly status: number;\n readonly code: LicensrErrorCode;\n /** Present on 429 responses when the server sends `Retry-After`. */\n readonly retryAfterSeconds: number | null;\n\n constructor(status: number, code: LicensrErrorCode, message: string, retryAfterSeconds: number | null = null) {\n super(message);\n this.name = 'LicensrApiError';\n this.status = status;\n this.code = code;\n this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\n/** The request never got a response — network failure, timeout, or abort. */\nexport class LicensrNetworkError extends Error {\n readonly cause: unknown;\n\n constructor(message: string, cause: unknown) {\n super(message);\n this.name = 'LicensrNetworkError';\n this.cause = cause;\n }\n}\n\n/** An offline token failed EdDSA verification, was malformed, or has expired. */\nexport class LicensrTokenVerificationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LicensrTokenVerificationError';\n }\n}\n","import {LicensrApiError, LicensrNetworkError} from './errors.js';\n\n/** SDKs already compute a stable per-machine HWID for activation — send it\n * here for free so the server buckets rate limits per *installation*\n * rather than per API key (see backend/services/rate_limit_key.py). */\nexport const DEVICE_ID_HEADER = 'X-Licensr-Device-Id';\n\nexport interface RetryOptions {\n /** Extra attempts after the first, on network errors / 429 / 5xx. Default 2. */\n maxRetries?: number;\n /** Base delay for exponential backoff with full jitter. Default 300ms. */\n baseDelayMs?: number;\n /** Ceiling for the backoff delay. Default 5000ms. */\n maxDelayMs?: number;\n}\n\nexport interface HttpClientConfig {\n baseUrl: string;\n apiKey: string;\n /**\n * Explicitly set the `Origin` header. Real browsers control this header\n * themselves — the fetch spec forbids scripts from overriding it, and\n * that's fine, since the browser already sends the caller's true origin.\n * This option exists for non-browser runtimes (Node, Electron main\n * process) that want the domain-origin guard (see docs/sdks) to see a\n * specific origin. Native/desktop plugins should instead set the\n * plugin's client type to \"native\" in the admin UI and leave this unset.\n */\n origin?: string;\n /** Stable per-install identifier — see {@link DEVICE_ID_HEADER}. */\n deviceId?: string;\n /** Defaults to `globalThis.fetch`. Override for testing or non-standard runtimes. */\n fetchImpl?: typeof fetch;\n /** Per-request timeout. Unset by default (no timeout). */\n timeoutMs?: number;\n retry?: RetryOptions;\n}\n\ninterface RequestSpec {\n method: 'GET' | 'POST';\n path: string;\n query?: Record<string, string | undefined>;\n body?: unknown;\n /** Called right before each retry's backoff delay — wired to the `retry` event in {@link import('./client.js').LicensrClient}. */\n onRetry?: (attempt: number, delayMs: number) => void;\n}\n\nconst DEFAULT_RETRY: Required<RetryOptions> = {\n maxRetries: 2,\n baseDelayMs: 300,\n maxDelayMs: 5000,\n};\n\nfunction buildUrl(baseUrl: string, path: string, query?: Record<string, string | undefined>): string {\n const url = new URL(path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n }\n return url.toString();\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exponential backoff with full jitter (AWS-style): `random(0, min(max, base * 2^attempt))`. */\nfunction jitterDelayMs(attempt: number, baseDelayMs: number, maxDelayMs: number): number {\n const cap = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);\n return Math.random() * cap;\n}\n\nfunction parseRetryAfterSeconds(header: string | null): number | null {\n if (!header) return null;\n const seconds = Number(header);\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;\n}\n\nasync function parseErrorDetail(response: Response): Promise<{code: string; message: string}> {\n let json: unknown;\n try {\n json = await response.json();\n } catch {\n return {code: 'unknown_error', message: response.statusText || `HTTP ${response.status}`};\n }\n\n const detail = (json as Record<string, unknown> | null)?.detail;\n\n // `{\"detail\": {\"error\": \"...\", \"message\": \"...\"}}` — every hand-written\n // HTTPException in the license/billing routers uses this shape.\n if (detail && typeof detail === 'object' && !Array.isArray(detail) && 'error' in detail) {\n const {error, message} = detail as {error?: unknown; message?: unknown};\n return {\n code: typeof error === 'string' ? error : 'unknown_error',\n message: typeof message === 'string' ? message : response.statusText,\n };\n }\n\n // `{\"detail\": [{\"msg\": \"...\", \"loc\": [...]}, ...]}` — FastAPI/Pydantic's\n // own 422 shape for request validation errors, not a hand-written one.\n if (Array.isArray(detail) && detail.length > 0) {\n const first = detail[0] as {msg?: unknown} | undefined;\n return {\n code: 'validation_error',\n message: typeof first?.msg === 'string' ? first.msg : 'Request validation failed',\n };\n }\n\n return {code: 'unknown_error', message: response.statusText || `HTTP ${response.status}`};\n}\n\n/**\n * Perform one logical API call, retrying network errors / 429 / 5xx with\n * exponential backoff + jitter, and honouring a `Retry-After` header when\n * present. Throws {@link LicensrApiError} for any other non-2xx response,\n * or {@link LicensrNetworkError} once retries are exhausted.\n */\nexport async function requestJson<T>(config: HttpClientConfig, spec: RequestSpec): Promise<T> {\n const fetchImpl = config.fetchImpl ?? globalThis.fetch;\n if (!fetchImpl) {\n throw new LicensrNetworkError(\n 'No fetch implementation available. Pass `fetchImpl` explicitly on Node < 18.',\n undefined\n );\n }\n const retry = {...DEFAULT_RETRY, ...config.retry};\n const url = buildUrl(config.baseUrl, spec.path, spec.query);\n\n for (let attempt = 0; ; attempt += 1) {\n const headers = new Headers({Authorization: `Bearer ${config.apiKey}`});\n if (spec.body !== undefined) headers.set('Content-Type', 'application/json');\n if (config.deviceId) headers.set(DEVICE_ID_HEADER, config.deviceId);\n if (config.origin) {\n try {\n headers.set('Origin', config.origin);\n } catch {\n // Forbidden header name in this runtime (real browsers) — the\n // browser already sends the caller's true Origin, so this is a\n // no-op rather than an error.\n }\n }\n\n const controller = config.timeoutMs !== undefined ? new AbortController() : undefined;\n const timeoutHandle =\n controller && config.timeoutMs !== undefined ? setTimeout(() => controller.abort(), config.timeoutMs) : undefined;\n\n let response: Response;\n try {\n response = await fetchImpl(url, {\n method: spec.method,\n headers,\n body: spec.body !== undefined ? JSON.stringify(spec.body) : undefined,\n signal: controller?.signal,\n });\n } catch (cause) {\n if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);\n if (attempt < retry.maxRetries) {\n const delayMs = jitterDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs);\n spec.onRetry?.(attempt + 1, delayMs);\n await sleep(delayMs);\n continue;\n }\n throw new LicensrNetworkError(`Request to ${spec.path} failed after ${attempt + 1} attempt(s)`, cause);\n }\n if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);\n\n if (response.ok) {\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n\n const retriable = response.status === 429 || response.status >= 500;\n if (retriable && attempt < retry.maxRetries) {\n const retryAfterSeconds = parseRetryAfterSeconds(response.headers.get('Retry-After'));\n const delayMs =\n retryAfterSeconds !== null\n ? retryAfterSeconds * 1000\n : jitterDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs);\n spec.onRetry?.(attempt + 1, delayMs);\n await sleep(delayMs);\n continue;\n }\n\n const {code, message} = await parseErrorDetail(response);\n throw new LicensrApiError(\n response.status,\n code,\n message,\n parseRetryAfterSeconds(response.headers.get('Retry-After'))\n );\n }\n}\n","/**\n * snake_case wire shapes for the license API and their mappers to/from the\n * SDK's public camelCase types. Kept explicit (no generic deep-casing) so\n * opaque, plugin-defined maps like `featureFlags` are never mangled.\n */\nimport type {\n ActivateResponse,\n ActivationListItem,\n ActivationsResponse,\n ActivationType,\n CheckoutResponse,\n DeactivateResponse,\n Entitlement,\n LicenseStatus,\n TokenResponse,\n ValidateResponse,\n} from './types.js';\n\nexport interface ValidateResponseWire {\n valid: boolean;\n status: LicenseStatus;\n plan_id: string | null;\n plan_name: string | null;\n activation_mode: ActivationType | null;\n max_seats: number | null;\n max_domains: number | null;\n seats_in_use: number | null;\n expires_at: string | null;\n entitlement: Entitlement;\n fallback: boolean;\n feature_flags: Record<string, unknown> | null;\n}\n\nexport function fromValidateWire(wire: ValidateResponseWire): ValidateResponse {\n return {\n valid: wire.valid,\n status: wire.status,\n planId: wire.plan_id,\n planName: wire.plan_name,\n activationMode: wire.activation_mode,\n maxSeats: wire.max_seats,\n maxDomains: wire.max_domains,\n seatsInUse: wire.seats_in_use,\n expiresAt: wire.expires_at,\n entitlement: wire.entitlement,\n fallback: wire.fallback,\n featureFlags: wire.feature_flags,\n };\n}\n\nexport interface TokenResponseWire {\n token: string;\n algorithm: string;\n kid: string;\n issued_at: string;\n expires_at: string;\n jwks_url: string;\n}\n\nexport function fromTokenWire(wire: TokenResponseWire): TokenResponse {\n return {\n token: wire.token,\n algorithm: wire.algorithm,\n kid: wire.kid,\n issuedAt: wire.issued_at,\n expiresAt: wire.expires_at,\n jwksUrl: wire.jwks_url,\n };\n}\n\nexport interface ActivateResponseWire {\n activation_id: string;\n activation_type: ActivationType;\n identifier: string;\n feature_flags: Record<string, unknown> | null;\n}\n\nexport function fromActivateWire(wire: ActivateResponseWire): ActivateResponse {\n return {\n activationId: wire.activation_id,\n activationType: wire.activation_type,\n identifier: wire.identifier,\n featureFlags: wire.feature_flags,\n };\n}\n\nexport interface DeactivateResponseWire {\n deactivated: boolean;\n}\n\nexport function fromDeactivateWire(wire: DeactivateResponseWire): DeactivateResponse {\n return {deactivated: wire.deactivated};\n}\n\nexport interface ActivationListItemWire {\n activation_id: string;\n activation_type: ActivationType;\n identifier: string;\n label: string | null;\n first_seen_at: string;\n last_seen_at: string;\n}\n\nexport interface ActivationsResponseWire {\n license_id: string;\n activations: ActivationListItemWire[];\n}\n\nfunction fromActivationListItemWire(wire: ActivationListItemWire): ActivationListItem {\n return {\n activationId: wire.activation_id,\n activationType: wire.activation_type,\n identifier: wire.identifier,\n label: wire.label,\n firstSeenAt: wire.first_seen_at,\n lastSeenAt: wire.last_seen_at,\n };\n}\n\nexport function fromActivationsWire(wire: ActivationsResponseWire): ActivationsResponse {\n return {\n licenseId: wire.license_id,\n activations: wire.activations.map(fromActivationListItemWire),\n };\n}\n\nexport interface CheckoutResponseWire {\n checkout_url: string;\n}\n\nexport function fromCheckoutWire(wire: CheckoutResponseWire): CheckoutResponse {\n return {checkoutUrl: wire.checkout_url};\n}\n","import {LicensrEventEmitter} from './events.js';\nimport {requestJson, type HttpClientConfig, type RetryOptions} from './http.js';\nimport type {\n ActivateRequest,\n ActivateResponse,\n ActivationsResponse,\n CheckoutRequest,\n CheckoutResponse,\n DeactivateRequest,\n DeactivateResponse,\n TokenResponse,\n ValidateRequest,\n ValidateResponse,\n} from './types.js';\nimport {\n fromActivateWire,\n fromActivationsWire,\n fromCheckoutWire,\n fromDeactivateWire,\n fromTokenWire,\n fromValidateWire,\n type ActivateResponseWire,\n type ActivationsResponseWire,\n type CheckoutResponseWire,\n type DeactivateResponseWire,\n type TokenResponseWire,\n type ValidateResponseWire,\n} from './wire.js';\n\nexport interface LicensrClientConfig {\n /** Plugin API key (`pk_live_...` / `pk_test_...`) — see docs/plugin-integration.md. */\n apiKey: string;\n /** The plugin this key belongs to (matches the slug in the admin dashboard). */\n pluginSlug: string;\n /** Defaults to `https://api.licensr.app`. Override for self-hosted/staging. */\n baseUrl?: string;\n /** See {@link HttpClientConfig.origin}. */\n origin?: string;\n /** Stable per-install identifier, e.g. the same HWID used for `activate()`. Buckets rate limits per installation instead of per key. */\n deviceId?: string;\n /** Defaults to `globalThis.fetch`. */\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n retry?: RetryOptions;\n}\n\n/**\n * Thin client for the Licensr public license API\n * (`/v1/license/*`, `/v1/billing/checkout`).\n *\n * ```ts\n * const client = new LicensrClient({apiKey: 'pk_live_...', pluginSlug: 'my-plugin'});\n * const result = await client.validate({licenseKey});\n * if (result.valid) { ... }\n * ```\n *\n * Subscribe to lifecycle events via `client.events`:\n *\n * ```ts\n * client.events.on('error', ({method, error}) => log(method, error));\n * ```\n */\nexport class LicensrClient {\n readonly events = new LicensrEventEmitter();\n private readonly http: HttpClientConfig;\n private readonly pluginSlug: string;\n\n constructor(config: LicensrClientConfig) {\n this.pluginSlug = config.pluginSlug;\n this.http = {\n baseUrl: config.baseUrl ?? 'https://api.licensr.app',\n apiKey: config.apiKey,\n origin: config.origin,\n deviceId: config.deviceId,\n fetchImpl: config.fetchImpl,\n timeoutMs: config.timeoutMs,\n retry: config.retry,\n };\n }\n\n /** Check whether a license is valid for this plugin, without minting an offline token. */\n async validate(req: ValidateRequest): Promise<ValidateResponse> {\n return this.run('validate', async () => {\n const wire = await requestJson<ValidateResponseWire>(this.http, {\n method: 'POST',\n path: 'v1/license/validate',\n body: {license_key: req.licenseKey, plugin_slug: this.pluginSlug},\n onRetry: this.onRetry('validate'),\n });\n return fromValidateWire(wire);\n });\n }\n\n /**\n * Run the same check as {@link validate}, and on success mint a\n * short-lived EdDSA-signed offline token. Verify it with\n * {@link import('./offline.js').verifyOfflineToken} — no network call\n * needed after the first successful fetch of the plugin's JWKS.\n */\n async token(req: ValidateRequest): Promise<TokenResponse> {\n return this.run('token', async () => {\n const wire = await requestJson<TokenResponseWire>(this.http, {\n method: 'POST',\n path: 'v1/license/token',\n body: {license_key: req.licenseKey, plugin_slug: this.pluginSlug},\n onRetry: this.onRetry('token'),\n });\n return fromTokenWire(wire);\n });\n }\n\n /** Activate a seat (per-machine HWID) or domain for a license. */\n async activate(req: ActivateRequest): Promise<ActivateResponse> {\n return this.run('activate', async () => {\n const wire = await requestJson<ActivateResponseWire>(this.http, {\n method: 'POST',\n path: 'v1/license/activate',\n body: {\n license_key: req.licenseKey,\n plugin_slug: this.pluginSlug,\n activation_type: req.activationType,\n identifier: req.identifier,\n label: req.label ?? null,\n },\n onRetry: this.onRetry('activate'),\n });\n return fromActivateWire(wire);\n });\n }\n\n /** Release a previously created activation, freeing its seat/domain slot. */\n async deactivate(req: DeactivateRequest): Promise<DeactivateResponse> {\n return this.run('deactivate', async () => {\n const wire = await requestJson<DeactivateResponseWire>(this.http, {\n method: 'POST',\n path: 'v1/license/deactivate',\n body: {license_key: req.licenseKey, activation_id: req.activationId},\n onRetry: this.onRetry('deactivate'),\n });\n return fromDeactivateWire(wire);\n });\n }\n\n /** List every current activation for a license (seats/domains in use). */\n async activations(req: {licenseKey: string}): Promise<ActivationsResponse> {\n return this.run('activations', async () => {\n const wire = await requestJson<ActivationsResponseWire>(this.http, {\n method: 'GET',\n path: 'v1/license/activations',\n query: {license_key: req.licenseKey, plugin_slug: this.pluginSlug},\n onRetry: this.onRetry('activations'),\n });\n return fromActivationsWire(wire);\n });\n }\n\n /** Start a hosted checkout session for a plan; redirect the user to the returned URL. */\n async checkout(req: CheckoutRequest): Promise<CheckoutResponse> {\n return this.run('checkout', async () => {\n const wire = await requestJson<CheckoutResponseWire>(this.http, {\n method: 'POST',\n path: 'v1/billing/checkout',\n body: {\n plan_id: req.planId,\n customer_email: req.customerEmail,\n success_url: req.successUrl ?? null,\n cancel_url: req.cancelUrl ?? null,\n },\n onRetry: this.onRetry('checkout'),\n });\n return fromCheckoutWire(wire);\n });\n }\n\n private onRetry(method: string): (attempt: number, delayMs: number) => void {\n return (attempt, delayMs) => this.events.emit('retry', {method, attempt, delayMs});\n }\n\n private async run<T>(method: string, fn: () => Promise<T>): Promise<T> {\n try {\n const result = await fn();\n this.emitResult(method, result);\n return result;\n } catch (error) {\n this.events.emit('error', {method, error});\n throw error;\n }\n }\n\n private emitResult(method: string, result: unknown): void {\n switch (method) {\n case 'validate':\n this.events.emit('validated', result as ValidateResponse);\n break;\n case 'activate':\n this.events.emit('activated', result as ActivateResponse);\n break;\n case 'deactivate':\n this.events.emit('deactivated', result as DeactivateResponse);\n break;\n case 'token':\n this.events.emit('tokenIssued', result as TokenResponse);\n break;\n default:\n break;\n }\n }\n}\n","import {createRemoteJWKSet, customFetch, jwtVerify} from 'jose';\nimport {LicensrTokenVerificationError} from './errors.js';\nimport type {OfflineTokenClaims} from './types.js';\n\nexport interface VerifyOfflineTokenOptions {\n /** Override the fetch used for the JWKS lookup — mainly for testing. */\n fetchImpl?: typeof fetch;\n /** Reject if the token's `iat`/`nbf` are further in the future than this many seconds (clock skew tolerance). Default 60. */\n clockToleranceSeconds?: number;\n}\n\n// `createRemoteJWKSet` already caches keys in-memory with a cooldown, so\n// reusing one instance per JWKS URL for the process lifetime avoids an\n// extra fetch on every verification while still picking up key rotation.\nconst jwksSetCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getJwks(jwksUrl: string, fetchImpl?: typeof fetch): ReturnType<typeof createRemoteJWKSet> {\n let jwks = jwksSetCache.get(jwksUrl);\n if (!jwks) {\n jwks = createRemoteJWKSet(\n new URL(jwksUrl),\n fetchImpl ? {[customFetch]: (url, options) => fetchImpl(url, options as RequestInit)} : undefined\n );\n jwksSetCache.set(jwksUrl, jwks);\n }\n return jwks;\n}\n\n/** Drop cached JWKS lookups — call after rotating a plugin's signing key in tests, or to force a fresh fetch. */\nexport function clearJwksCache(): void {\n jwksSetCache.clear();\n}\n\n/**\n * Verify an offline license token (from {@link import('./client.js').LicensrClient.token})\n * fully offline: no network call beyond fetching (and caching) the plugin's\n * public JWKS. Throws {@link LicensrTokenVerificationError} on a bad\n * signature, wrong algorithm, or an expired/not-yet-valid token.\n *\n * Tokens carry no revocation signal — this only proves the token was\n * genuinely issued by Licensr and hasn't expired, not that the license is\n * still active right now. Re-validate online (`validate()`/`token()`)\n * before the token's `expiresAt`.\n */\nexport async function verifyOfflineToken(\n token: string,\n jwksUrl: string,\n options: VerifyOfflineTokenOptions = {}\n): Promise<OfflineTokenClaims> {\n const jwks = getJwks(jwksUrl, options.fetchImpl);\n try {\n const {payload} = await jwtVerify(token, jwks, {\n algorithms: ['EdDSA'],\n clockTolerance: options.clockToleranceSeconds ?? 60,\n });\n return payload as unknown as OfflineTokenClaims;\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n throw new LicensrTokenVerificationError(`Offline token verification failed: ${reason}`);\n }\n}\n\nexport interface CachedOfflineToken {\n token: string;\n claims: OfflineTokenClaims;\n /** `Date.now()` at the time this entry was cached. */\n cachedAtMs: number;\n}\n\n/**\n * Pluggable persistence for the last-known-good offline token per license,\n * so an app can boot fully offline (no network at all) before its first\n * successful `/token` call in a given session. The default\n * {@link InMemoryOfflineTokenStore} does not persist across process\n * restarts — provide your own (backed by `localStorage`, a config file,\n * OS keychain, etc.) for that.\n */\nexport interface OfflineTokenStore {\n get(licenseKey: string): CachedOfflineToken | null | Promise<CachedOfflineToken | null>;\n set(licenseKey: string, entry: CachedOfflineToken): void | Promise<void>;\n clear(licenseKey: string): void | Promise<void>;\n}\n\nexport class InMemoryOfflineTokenStore implements OfflineTokenStore {\n private readonly entries = new Map<string, CachedOfflineToken>();\n\n get(licenseKey: string): CachedOfflineToken | null {\n return this.entries.get(licenseKey) ?? null;\n }\n\n set(licenseKey: string, entry: CachedOfflineToken): void {\n this.entries.set(licenseKey, entry);\n }\n\n clear(licenseKey: string): void {\n this.entries.delete(licenseKey);\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@licensr/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official JavaScript/TypeScript SDK for the Licensr license API — activation, validation, offline EdDSA token verification.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "test": "vitest run",
30
+ "test:coverage": "vitest run --coverage",
31
+ "test:watch": "vitest",
32
+ "typecheck": "tsc --noEmit",
33
+ "lint": "eslint src test",
34
+ "lint:fix": "eslint src test --fix",
35
+ "format": "prettier --write .",
36
+ "check": "tsc --noEmit && eslint src test && prettier --check ."
37
+ },
38
+ "dependencies": {
39
+ "jose": "^6.2.8"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^10.0.1",
43
+ "@types/node": "^25.7.0",
44
+ "@vitest/coverage-v8": "^4.1.6",
45
+ "eslint": "^10.8.0",
46
+ "eslint-config-prettier": "^10.1.8",
47
+ "prettier": "^3.8.3",
48
+ "tsup": "^8.5.1",
49
+ "typescript": "^6.0.3",
50
+ "typescript-eslint": "^8.66.0",
51
+ "vitest": "^4.1.6",
52
+ "yaml": "^2.9.0"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/licensr/licensr-js.git",
57
+ "directory": "sdks/js"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "provenance": true
62
+ },
63
+ "homepage": "https://docs.licensr.app/sdks/js",
64
+ "keywords": [
65
+ "licensr",
66
+ "license",
67
+ "licensing",
68
+ "software-licensing",
69
+ "activation",
70
+ "sdk"
71
+ ]
72
+ }