@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/README.md +143 -0
- package/dist/index.cjs +413 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +309 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.js +403 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request/response shapes for the Licensr public license API.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors ``contract/openapi.json`` (generated from the FastAPI backend —
|
|
5
|
+
* see ``backend/services/models/license.py``). Keep in sync manually:
|
|
6
|
+
* there is no codegen step, so any field added to the backend's response
|
|
7
|
+
* models should be mirrored here.
|
|
8
|
+
*/
|
|
9
|
+
type ActivationType = 'seat' | 'domain';
|
|
10
|
+
type LicenseStatus = 'active' | 'inactive' | 'expired';
|
|
11
|
+
type Entitlement = 'full' | 'limited' | 'none';
|
|
12
|
+
interface ValidateRequest {
|
|
13
|
+
licenseKey: string;
|
|
14
|
+
}
|
|
15
|
+
interface ValidateResponse {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
status: LicenseStatus;
|
|
18
|
+
planId: string | null;
|
|
19
|
+
planName: string | null;
|
|
20
|
+
activationMode: ActivationType | null;
|
|
21
|
+
maxSeats: number | null;
|
|
22
|
+
maxDomains: number | null;
|
|
23
|
+
seatsInUse: number | null;
|
|
24
|
+
expiresAt: string | null;
|
|
25
|
+
/** `full` when valid, `none` when cut off, `limited` alongside `fallback: true`. */
|
|
26
|
+
entitlement: Entitlement;
|
|
27
|
+
/** `true` only when an *expired* license is on a perpetual-fallback plan. */
|
|
28
|
+
fallback: boolean;
|
|
29
|
+
featureFlags: Record<string, unknown> | null;
|
|
30
|
+
}
|
|
31
|
+
interface ActivateRequest {
|
|
32
|
+
licenseKey: string;
|
|
33
|
+
activationType: ActivationType;
|
|
34
|
+
/** Stable per-install identifier: HWID for `seat`, hostname for `domain`. */
|
|
35
|
+
identifier: string;
|
|
36
|
+
/** Cosmetic, shown in admin/customer portals (e.g. "My MacBook Pro"). */
|
|
37
|
+
label?: string | null;
|
|
38
|
+
}
|
|
39
|
+
interface ActivateResponse {
|
|
40
|
+
activationId: string;
|
|
41
|
+
activationType: ActivationType;
|
|
42
|
+
identifier: string;
|
|
43
|
+
featureFlags: Record<string, unknown> | null;
|
|
44
|
+
}
|
|
45
|
+
interface DeactivateRequest {
|
|
46
|
+
licenseKey: string;
|
|
47
|
+
activationId: string;
|
|
48
|
+
}
|
|
49
|
+
interface DeactivateResponse {
|
|
50
|
+
deactivated: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface ActivationListItem {
|
|
53
|
+
activationId: string;
|
|
54
|
+
activationType: ActivationType;
|
|
55
|
+
identifier: string;
|
|
56
|
+
label: string | null;
|
|
57
|
+
firstSeenAt: string;
|
|
58
|
+
lastSeenAt: string;
|
|
59
|
+
}
|
|
60
|
+
interface ActivationsRequest {
|
|
61
|
+
licenseKey: string;
|
|
62
|
+
}
|
|
63
|
+
interface ActivationsResponse {
|
|
64
|
+
licenseId: string;
|
|
65
|
+
activations: ActivationListItem[];
|
|
66
|
+
}
|
|
67
|
+
interface TokenResponse {
|
|
68
|
+
/** EdDSA-signed JWT — verify with {@link verifyOfflineToken} or `jose` directly. */
|
|
69
|
+
token: string;
|
|
70
|
+
algorithm: string;
|
|
71
|
+
kid: string;
|
|
72
|
+
issuedAt: string;
|
|
73
|
+
expiresAt: string;
|
|
74
|
+
jwksUrl: string;
|
|
75
|
+
}
|
|
76
|
+
/** Claims embedded in the offline token (`token`); mirrors {@link ValidateResponse}. */
|
|
77
|
+
interface OfflineTokenClaims {
|
|
78
|
+
sub: string;
|
|
79
|
+
iss: string;
|
|
80
|
+
iat: number;
|
|
81
|
+
nbf: number;
|
|
82
|
+
exp: number;
|
|
83
|
+
plugin_slug: string;
|
|
84
|
+
plan_id: string | null;
|
|
85
|
+
status: LicenseStatus;
|
|
86
|
+
valid: boolean;
|
|
87
|
+
entitlement: Entitlement;
|
|
88
|
+
fallback: boolean;
|
|
89
|
+
activation_mode: ActivationType | null;
|
|
90
|
+
max_seats: number | null;
|
|
91
|
+
max_domains: number | null;
|
|
92
|
+
license_expires_at: number | null;
|
|
93
|
+
feature_flags: Record<string, unknown> | null;
|
|
94
|
+
}
|
|
95
|
+
interface CheckoutRequest {
|
|
96
|
+
planId: string;
|
|
97
|
+
customerEmail: string;
|
|
98
|
+
successUrl?: string | null;
|
|
99
|
+
cancelUrl?: string | null;
|
|
100
|
+
}
|
|
101
|
+
interface CheckoutResponse {
|
|
102
|
+
checkoutUrl: string;
|
|
103
|
+
}
|
|
104
|
+
/** Every documented `detail.error` code from the license API — see contract/conformance.yaml. */
|
|
105
|
+
type LicensrErrorCode = 'missing_plugin_api_key' | 'invalid_plugin_api_key' | 'origin_not_allowed' | 'origin_not_activated' | 'plugin_mismatch' | 'license_inactive' | 'license_not_valid' | 'license_not_found' | 'activation_not_found' | 'activation_mode_mismatch' | 'activation_cap_exceeded' | 'jwks_path_deprecated' | 'rate_limit_exceeded' | 'insufficient_scope' | (string & {});
|
|
106
|
+
interface ErrorBody {
|
|
107
|
+
error: LicensrErrorCode;
|
|
108
|
+
message?: string | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Lifecycle events emitted by {@link import('./client.js').LicensrClient}. */
|
|
112
|
+
interface LicensrEventMap {
|
|
113
|
+
validated: ValidateResponse;
|
|
114
|
+
activated: ActivateResponse;
|
|
115
|
+
deactivated: DeactivateResponse;
|
|
116
|
+
tokenIssued: TokenResponse;
|
|
117
|
+
/** Fired on every retried request, before the retry delay. */
|
|
118
|
+
retry: {
|
|
119
|
+
method: string;
|
|
120
|
+
attempt: number;
|
|
121
|
+
delayMs: number;
|
|
122
|
+
};
|
|
123
|
+
/** Fired whenever a client method throws, right before the error propagates. */
|
|
124
|
+
error: {
|
|
125
|
+
method: string;
|
|
126
|
+
error: unknown;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
type Listener<T> = (payload: T) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Minimal, dependency-free typed event emitter for {@link LicensrEventMap}
|
|
132
|
+
* — no Node `EventEmitter` import so the SDK stays usable unmodified in
|
|
133
|
+
* browsers.
|
|
134
|
+
*/
|
|
135
|
+
declare class LicensrEventEmitter {
|
|
136
|
+
private readonly listeners;
|
|
137
|
+
/** Returns an unsubscribe function. */
|
|
138
|
+
on<K extends keyof LicensrEventMap>(event: K, listener: Listener<LicensrEventMap[K]>): () => void;
|
|
139
|
+
off<K extends keyof LicensrEventMap>(event: K, listener: Listener<LicensrEventMap[K]>): void;
|
|
140
|
+
emit<K extends keyof LicensrEventMap>(event: K, payload: LicensrEventMap[K]): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** SDKs already compute a stable per-machine HWID for activation — send it
|
|
144
|
+
* here for free so the server buckets rate limits per *installation*
|
|
145
|
+
* rather than per API key (see backend/services/rate_limit_key.py). */
|
|
146
|
+
declare const DEVICE_ID_HEADER = "X-Licensr-Device-Id";
|
|
147
|
+
interface RetryOptions {
|
|
148
|
+
/** Extra attempts after the first, on network errors / 429 / 5xx. Default 2. */
|
|
149
|
+
maxRetries?: number;
|
|
150
|
+
/** Base delay for exponential backoff with full jitter. Default 300ms. */
|
|
151
|
+
baseDelayMs?: number;
|
|
152
|
+
/** Ceiling for the backoff delay. Default 5000ms. */
|
|
153
|
+
maxDelayMs?: number;
|
|
154
|
+
}
|
|
155
|
+
interface HttpClientConfig {
|
|
156
|
+
baseUrl: string;
|
|
157
|
+
apiKey: string;
|
|
158
|
+
/**
|
|
159
|
+
* Explicitly set the `Origin` header. Real browsers control this header
|
|
160
|
+
* themselves — the fetch spec forbids scripts from overriding it, and
|
|
161
|
+
* that's fine, since the browser already sends the caller's true origin.
|
|
162
|
+
* This option exists for non-browser runtimes (Node, Electron main
|
|
163
|
+
* process) that want the domain-origin guard (see docs/sdks) to see a
|
|
164
|
+
* specific origin. Native/desktop plugins should instead set the
|
|
165
|
+
* plugin's client type to "native" in the admin UI and leave this unset.
|
|
166
|
+
*/
|
|
167
|
+
origin?: string;
|
|
168
|
+
/** Stable per-install identifier — see {@link DEVICE_ID_HEADER}. */
|
|
169
|
+
deviceId?: string;
|
|
170
|
+
/** Defaults to `globalThis.fetch`. Override for testing or non-standard runtimes. */
|
|
171
|
+
fetchImpl?: typeof fetch;
|
|
172
|
+
/** Per-request timeout. Unset by default (no timeout). */
|
|
173
|
+
timeoutMs?: number;
|
|
174
|
+
retry?: RetryOptions;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface LicensrClientConfig {
|
|
178
|
+
/** Plugin API key (`pk_live_...` / `pk_test_...`) — see docs/plugin-integration.md. */
|
|
179
|
+
apiKey: string;
|
|
180
|
+
/** The plugin this key belongs to (matches the slug in the admin dashboard). */
|
|
181
|
+
pluginSlug: string;
|
|
182
|
+
/** Defaults to `https://api.licensr.app`. Override for self-hosted/staging. */
|
|
183
|
+
baseUrl?: string;
|
|
184
|
+
/** See {@link HttpClientConfig.origin}. */
|
|
185
|
+
origin?: string;
|
|
186
|
+
/** Stable per-install identifier, e.g. the same HWID used for `activate()`. Buckets rate limits per installation instead of per key. */
|
|
187
|
+
deviceId?: string;
|
|
188
|
+
/** Defaults to `globalThis.fetch`. */
|
|
189
|
+
fetchImpl?: typeof fetch;
|
|
190
|
+
timeoutMs?: number;
|
|
191
|
+
retry?: RetryOptions;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Thin client for the Licensr public license API
|
|
195
|
+
* (`/v1/license/*`, `/v1/billing/checkout`).
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* const client = new LicensrClient({apiKey: 'pk_live_...', pluginSlug: 'my-plugin'});
|
|
199
|
+
* const result = await client.validate({licenseKey});
|
|
200
|
+
* if (result.valid) { ... }
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* Subscribe to lifecycle events via `client.events`:
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* client.events.on('error', ({method, error}) => log(method, error));
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
declare class LicensrClient {
|
|
210
|
+
readonly events: LicensrEventEmitter;
|
|
211
|
+
private readonly http;
|
|
212
|
+
private readonly pluginSlug;
|
|
213
|
+
constructor(config: LicensrClientConfig);
|
|
214
|
+
/** Check whether a license is valid for this plugin, without minting an offline token. */
|
|
215
|
+
validate(req: ValidateRequest): Promise<ValidateResponse>;
|
|
216
|
+
/**
|
|
217
|
+
* Run the same check as {@link validate}, and on success mint a
|
|
218
|
+
* short-lived EdDSA-signed offline token. Verify it with
|
|
219
|
+
* {@link import('./offline.js').verifyOfflineToken} — no network call
|
|
220
|
+
* needed after the first successful fetch of the plugin's JWKS.
|
|
221
|
+
*/
|
|
222
|
+
token(req: ValidateRequest): Promise<TokenResponse>;
|
|
223
|
+
/** Activate a seat (per-machine HWID) or domain for a license. */
|
|
224
|
+
activate(req: ActivateRequest): Promise<ActivateResponse>;
|
|
225
|
+
/** Release a previously created activation, freeing its seat/domain slot. */
|
|
226
|
+
deactivate(req: DeactivateRequest): Promise<DeactivateResponse>;
|
|
227
|
+
/** List every current activation for a license (seats/domains in use). */
|
|
228
|
+
activations(req: {
|
|
229
|
+
licenseKey: string;
|
|
230
|
+
}): Promise<ActivationsResponse>;
|
|
231
|
+
/** Start a hosted checkout session for a plan; redirect the user to the returned URL. */
|
|
232
|
+
checkout(req: CheckoutRequest): Promise<CheckoutResponse>;
|
|
233
|
+
private onRetry;
|
|
234
|
+
private run;
|
|
235
|
+
private emitResult;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The API responded with a non-2xx status and a parsed `detail.error` body.
|
|
240
|
+
*
|
|
241
|
+
* `code` is one of the documented error codes in `contract/conformance.yaml`
|
|
242
|
+
* (e.g. `license_inactive`, `origin_not_allowed`, `rate_limit_exceeded`).
|
|
243
|
+
* Branch on `code`, not on `message` — the message is for humans and may
|
|
244
|
+
* change without notice.
|
|
245
|
+
*/
|
|
246
|
+
declare class LicensrApiError extends Error {
|
|
247
|
+
readonly status: number;
|
|
248
|
+
readonly code: LicensrErrorCode;
|
|
249
|
+
/** Present on 429 responses when the server sends `Retry-After`. */
|
|
250
|
+
readonly retryAfterSeconds: number | null;
|
|
251
|
+
constructor(status: number, code: LicensrErrorCode, message: string, retryAfterSeconds?: number | null);
|
|
252
|
+
}
|
|
253
|
+
/** The request never got a response — network failure, timeout, or abort. */
|
|
254
|
+
declare class LicensrNetworkError extends Error {
|
|
255
|
+
readonly cause: unknown;
|
|
256
|
+
constructor(message: string, cause: unknown);
|
|
257
|
+
}
|
|
258
|
+
/** An offline token failed EdDSA verification, was malformed, or has expired. */
|
|
259
|
+
declare class LicensrTokenVerificationError extends Error {
|
|
260
|
+
constructor(message: string);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface VerifyOfflineTokenOptions {
|
|
264
|
+
/** Override the fetch used for the JWKS lookup — mainly for testing. */
|
|
265
|
+
fetchImpl?: typeof fetch;
|
|
266
|
+
/** Reject if the token's `iat`/`nbf` are further in the future than this many seconds (clock skew tolerance). Default 60. */
|
|
267
|
+
clockToleranceSeconds?: number;
|
|
268
|
+
}
|
|
269
|
+
/** Drop cached JWKS lookups — call after rotating a plugin's signing key in tests, or to force a fresh fetch. */
|
|
270
|
+
declare function clearJwksCache(): void;
|
|
271
|
+
/**
|
|
272
|
+
* Verify an offline license token (from {@link import('./client.js').LicensrClient.token})
|
|
273
|
+
* fully offline: no network call beyond fetching (and caching) the plugin's
|
|
274
|
+
* public JWKS. Throws {@link LicensrTokenVerificationError} on a bad
|
|
275
|
+
* signature, wrong algorithm, or an expired/not-yet-valid token.
|
|
276
|
+
*
|
|
277
|
+
* Tokens carry no revocation signal — this only proves the token was
|
|
278
|
+
* genuinely issued by Licensr and hasn't expired, not that the license is
|
|
279
|
+
* still active right now. Re-validate online (`validate()`/`token()`)
|
|
280
|
+
* before the token's `expiresAt`.
|
|
281
|
+
*/
|
|
282
|
+
declare function verifyOfflineToken(token: string, jwksUrl: string, options?: VerifyOfflineTokenOptions): Promise<OfflineTokenClaims>;
|
|
283
|
+
interface CachedOfflineToken {
|
|
284
|
+
token: string;
|
|
285
|
+
claims: OfflineTokenClaims;
|
|
286
|
+
/** `Date.now()` at the time this entry was cached. */
|
|
287
|
+
cachedAtMs: number;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Pluggable persistence for the last-known-good offline token per license,
|
|
291
|
+
* so an app can boot fully offline (no network at all) before its first
|
|
292
|
+
* successful `/token` call in a given session. The default
|
|
293
|
+
* {@link InMemoryOfflineTokenStore} does not persist across process
|
|
294
|
+
* restarts — provide your own (backed by `localStorage`, a config file,
|
|
295
|
+
* OS keychain, etc.) for that.
|
|
296
|
+
*/
|
|
297
|
+
interface OfflineTokenStore {
|
|
298
|
+
get(licenseKey: string): CachedOfflineToken | null | Promise<CachedOfflineToken | null>;
|
|
299
|
+
set(licenseKey: string, entry: CachedOfflineToken): void | Promise<void>;
|
|
300
|
+
clear(licenseKey: string): void | Promise<void>;
|
|
301
|
+
}
|
|
302
|
+
declare class InMemoryOfflineTokenStore implements OfflineTokenStore {
|
|
303
|
+
private readonly entries;
|
|
304
|
+
get(licenseKey: string): CachedOfflineToken | null;
|
|
305
|
+
set(licenseKey: string, entry: CachedOfflineToken): void;
|
|
306
|
+
clear(licenseKey: string): void;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export { type ActivateRequest, type ActivateResponse, type ActivationListItem, type ActivationType, type ActivationsRequest, type ActivationsResponse, type CachedOfflineToken, type CheckoutRequest, type CheckoutResponse, DEVICE_ID_HEADER, type DeactivateRequest, type DeactivateResponse, type Entitlement, type ErrorBody, type HttpClientConfig, InMemoryOfflineTokenStore, type LicenseStatus, LicensrApiError, LicensrClient, type LicensrClientConfig, type LicensrErrorCode, LicensrEventEmitter, type LicensrEventMap, LicensrNetworkError, LicensrTokenVerificationError, type OfflineTokenClaims, type OfflineTokenStore, type RetryOptions, type TokenResponse, type ValidateRequest, type ValidateResponse, type VerifyOfflineTokenOptions, clearJwksCache, verifyOfflineToken };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request/response shapes for the Licensr public license API.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors ``contract/openapi.json`` (generated from the FastAPI backend —
|
|
5
|
+
* see ``backend/services/models/license.py``). Keep in sync manually:
|
|
6
|
+
* there is no codegen step, so any field added to the backend's response
|
|
7
|
+
* models should be mirrored here.
|
|
8
|
+
*/
|
|
9
|
+
type ActivationType = 'seat' | 'domain';
|
|
10
|
+
type LicenseStatus = 'active' | 'inactive' | 'expired';
|
|
11
|
+
type Entitlement = 'full' | 'limited' | 'none';
|
|
12
|
+
interface ValidateRequest {
|
|
13
|
+
licenseKey: string;
|
|
14
|
+
}
|
|
15
|
+
interface ValidateResponse {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
status: LicenseStatus;
|
|
18
|
+
planId: string | null;
|
|
19
|
+
planName: string | null;
|
|
20
|
+
activationMode: ActivationType | null;
|
|
21
|
+
maxSeats: number | null;
|
|
22
|
+
maxDomains: number | null;
|
|
23
|
+
seatsInUse: number | null;
|
|
24
|
+
expiresAt: string | null;
|
|
25
|
+
/** `full` when valid, `none` when cut off, `limited` alongside `fallback: true`. */
|
|
26
|
+
entitlement: Entitlement;
|
|
27
|
+
/** `true` only when an *expired* license is on a perpetual-fallback plan. */
|
|
28
|
+
fallback: boolean;
|
|
29
|
+
featureFlags: Record<string, unknown> | null;
|
|
30
|
+
}
|
|
31
|
+
interface ActivateRequest {
|
|
32
|
+
licenseKey: string;
|
|
33
|
+
activationType: ActivationType;
|
|
34
|
+
/** Stable per-install identifier: HWID for `seat`, hostname for `domain`. */
|
|
35
|
+
identifier: string;
|
|
36
|
+
/** Cosmetic, shown in admin/customer portals (e.g. "My MacBook Pro"). */
|
|
37
|
+
label?: string | null;
|
|
38
|
+
}
|
|
39
|
+
interface ActivateResponse {
|
|
40
|
+
activationId: string;
|
|
41
|
+
activationType: ActivationType;
|
|
42
|
+
identifier: string;
|
|
43
|
+
featureFlags: Record<string, unknown> | null;
|
|
44
|
+
}
|
|
45
|
+
interface DeactivateRequest {
|
|
46
|
+
licenseKey: string;
|
|
47
|
+
activationId: string;
|
|
48
|
+
}
|
|
49
|
+
interface DeactivateResponse {
|
|
50
|
+
deactivated: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface ActivationListItem {
|
|
53
|
+
activationId: string;
|
|
54
|
+
activationType: ActivationType;
|
|
55
|
+
identifier: string;
|
|
56
|
+
label: string | null;
|
|
57
|
+
firstSeenAt: string;
|
|
58
|
+
lastSeenAt: string;
|
|
59
|
+
}
|
|
60
|
+
interface ActivationsRequest {
|
|
61
|
+
licenseKey: string;
|
|
62
|
+
}
|
|
63
|
+
interface ActivationsResponse {
|
|
64
|
+
licenseId: string;
|
|
65
|
+
activations: ActivationListItem[];
|
|
66
|
+
}
|
|
67
|
+
interface TokenResponse {
|
|
68
|
+
/** EdDSA-signed JWT — verify with {@link verifyOfflineToken} or `jose` directly. */
|
|
69
|
+
token: string;
|
|
70
|
+
algorithm: string;
|
|
71
|
+
kid: string;
|
|
72
|
+
issuedAt: string;
|
|
73
|
+
expiresAt: string;
|
|
74
|
+
jwksUrl: string;
|
|
75
|
+
}
|
|
76
|
+
/** Claims embedded in the offline token (`token`); mirrors {@link ValidateResponse}. */
|
|
77
|
+
interface OfflineTokenClaims {
|
|
78
|
+
sub: string;
|
|
79
|
+
iss: string;
|
|
80
|
+
iat: number;
|
|
81
|
+
nbf: number;
|
|
82
|
+
exp: number;
|
|
83
|
+
plugin_slug: string;
|
|
84
|
+
plan_id: string | null;
|
|
85
|
+
status: LicenseStatus;
|
|
86
|
+
valid: boolean;
|
|
87
|
+
entitlement: Entitlement;
|
|
88
|
+
fallback: boolean;
|
|
89
|
+
activation_mode: ActivationType | null;
|
|
90
|
+
max_seats: number | null;
|
|
91
|
+
max_domains: number | null;
|
|
92
|
+
license_expires_at: number | null;
|
|
93
|
+
feature_flags: Record<string, unknown> | null;
|
|
94
|
+
}
|
|
95
|
+
interface CheckoutRequest {
|
|
96
|
+
planId: string;
|
|
97
|
+
customerEmail: string;
|
|
98
|
+
successUrl?: string | null;
|
|
99
|
+
cancelUrl?: string | null;
|
|
100
|
+
}
|
|
101
|
+
interface CheckoutResponse {
|
|
102
|
+
checkoutUrl: string;
|
|
103
|
+
}
|
|
104
|
+
/** Every documented `detail.error` code from the license API — see contract/conformance.yaml. */
|
|
105
|
+
type LicensrErrorCode = 'missing_plugin_api_key' | 'invalid_plugin_api_key' | 'origin_not_allowed' | 'origin_not_activated' | 'plugin_mismatch' | 'license_inactive' | 'license_not_valid' | 'license_not_found' | 'activation_not_found' | 'activation_mode_mismatch' | 'activation_cap_exceeded' | 'jwks_path_deprecated' | 'rate_limit_exceeded' | 'insufficient_scope' | (string & {});
|
|
106
|
+
interface ErrorBody {
|
|
107
|
+
error: LicensrErrorCode;
|
|
108
|
+
message?: string | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Lifecycle events emitted by {@link import('./client.js').LicensrClient}. */
|
|
112
|
+
interface LicensrEventMap {
|
|
113
|
+
validated: ValidateResponse;
|
|
114
|
+
activated: ActivateResponse;
|
|
115
|
+
deactivated: DeactivateResponse;
|
|
116
|
+
tokenIssued: TokenResponse;
|
|
117
|
+
/** Fired on every retried request, before the retry delay. */
|
|
118
|
+
retry: {
|
|
119
|
+
method: string;
|
|
120
|
+
attempt: number;
|
|
121
|
+
delayMs: number;
|
|
122
|
+
};
|
|
123
|
+
/** Fired whenever a client method throws, right before the error propagates. */
|
|
124
|
+
error: {
|
|
125
|
+
method: string;
|
|
126
|
+
error: unknown;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
type Listener<T> = (payload: T) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Minimal, dependency-free typed event emitter for {@link LicensrEventMap}
|
|
132
|
+
* — no Node `EventEmitter` import so the SDK stays usable unmodified in
|
|
133
|
+
* browsers.
|
|
134
|
+
*/
|
|
135
|
+
declare class LicensrEventEmitter {
|
|
136
|
+
private readonly listeners;
|
|
137
|
+
/** Returns an unsubscribe function. */
|
|
138
|
+
on<K extends keyof LicensrEventMap>(event: K, listener: Listener<LicensrEventMap[K]>): () => void;
|
|
139
|
+
off<K extends keyof LicensrEventMap>(event: K, listener: Listener<LicensrEventMap[K]>): void;
|
|
140
|
+
emit<K extends keyof LicensrEventMap>(event: K, payload: LicensrEventMap[K]): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** SDKs already compute a stable per-machine HWID for activation — send it
|
|
144
|
+
* here for free so the server buckets rate limits per *installation*
|
|
145
|
+
* rather than per API key (see backend/services/rate_limit_key.py). */
|
|
146
|
+
declare const DEVICE_ID_HEADER = "X-Licensr-Device-Id";
|
|
147
|
+
interface RetryOptions {
|
|
148
|
+
/** Extra attempts after the first, on network errors / 429 / 5xx. Default 2. */
|
|
149
|
+
maxRetries?: number;
|
|
150
|
+
/** Base delay for exponential backoff with full jitter. Default 300ms. */
|
|
151
|
+
baseDelayMs?: number;
|
|
152
|
+
/** Ceiling for the backoff delay. Default 5000ms. */
|
|
153
|
+
maxDelayMs?: number;
|
|
154
|
+
}
|
|
155
|
+
interface HttpClientConfig {
|
|
156
|
+
baseUrl: string;
|
|
157
|
+
apiKey: string;
|
|
158
|
+
/**
|
|
159
|
+
* Explicitly set the `Origin` header. Real browsers control this header
|
|
160
|
+
* themselves — the fetch spec forbids scripts from overriding it, and
|
|
161
|
+
* that's fine, since the browser already sends the caller's true origin.
|
|
162
|
+
* This option exists for non-browser runtimes (Node, Electron main
|
|
163
|
+
* process) that want the domain-origin guard (see docs/sdks) to see a
|
|
164
|
+
* specific origin. Native/desktop plugins should instead set the
|
|
165
|
+
* plugin's client type to "native" in the admin UI and leave this unset.
|
|
166
|
+
*/
|
|
167
|
+
origin?: string;
|
|
168
|
+
/** Stable per-install identifier — see {@link DEVICE_ID_HEADER}. */
|
|
169
|
+
deviceId?: string;
|
|
170
|
+
/** Defaults to `globalThis.fetch`. Override for testing or non-standard runtimes. */
|
|
171
|
+
fetchImpl?: typeof fetch;
|
|
172
|
+
/** Per-request timeout. Unset by default (no timeout). */
|
|
173
|
+
timeoutMs?: number;
|
|
174
|
+
retry?: RetryOptions;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface LicensrClientConfig {
|
|
178
|
+
/** Plugin API key (`pk_live_...` / `pk_test_...`) — see docs/plugin-integration.md. */
|
|
179
|
+
apiKey: string;
|
|
180
|
+
/** The plugin this key belongs to (matches the slug in the admin dashboard). */
|
|
181
|
+
pluginSlug: string;
|
|
182
|
+
/** Defaults to `https://api.licensr.app`. Override for self-hosted/staging. */
|
|
183
|
+
baseUrl?: string;
|
|
184
|
+
/** See {@link HttpClientConfig.origin}. */
|
|
185
|
+
origin?: string;
|
|
186
|
+
/** Stable per-install identifier, e.g. the same HWID used for `activate()`. Buckets rate limits per installation instead of per key. */
|
|
187
|
+
deviceId?: string;
|
|
188
|
+
/** Defaults to `globalThis.fetch`. */
|
|
189
|
+
fetchImpl?: typeof fetch;
|
|
190
|
+
timeoutMs?: number;
|
|
191
|
+
retry?: RetryOptions;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Thin client for the Licensr public license API
|
|
195
|
+
* (`/v1/license/*`, `/v1/billing/checkout`).
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* const client = new LicensrClient({apiKey: 'pk_live_...', pluginSlug: 'my-plugin'});
|
|
199
|
+
* const result = await client.validate({licenseKey});
|
|
200
|
+
* if (result.valid) { ... }
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* Subscribe to lifecycle events via `client.events`:
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* client.events.on('error', ({method, error}) => log(method, error));
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
declare class LicensrClient {
|
|
210
|
+
readonly events: LicensrEventEmitter;
|
|
211
|
+
private readonly http;
|
|
212
|
+
private readonly pluginSlug;
|
|
213
|
+
constructor(config: LicensrClientConfig);
|
|
214
|
+
/** Check whether a license is valid for this plugin, without minting an offline token. */
|
|
215
|
+
validate(req: ValidateRequest): Promise<ValidateResponse>;
|
|
216
|
+
/**
|
|
217
|
+
* Run the same check as {@link validate}, and on success mint a
|
|
218
|
+
* short-lived EdDSA-signed offline token. Verify it with
|
|
219
|
+
* {@link import('./offline.js').verifyOfflineToken} — no network call
|
|
220
|
+
* needed after the first successful fetch of the plugin's JWKS.
|
|
221
|
+
*/
|
|
222
|
+
token(req: ValidateRequest): Promise<TokenResponse>;
|
|
223
|
+
/** Activate a seat (per-machine HWID) or domain for a license. */
|
|
224
|
+
activate(req: ActivateRequest): Promise<ActivateResponse>;
|
|
225
|
+
/** Release a previously created activation, freeing its seat/domain slot. */
|
|
226
|
+
deactivate(req: DeactivateRequest): Promise<DeactivateResponse>;
|
|
227
|
+
/** List every current activation for a license (seats/domains in use). */
|
|
228
|
+
activations(req: {
|
|
229
|
+
licenseKey: string;
|
|
230
|
+
}): Promise<ActivationsResponse>;
|
|
231
|
+
/** Start a hosted checkout session for a plan; redirect the user to the returned URL. */
|
|
232
|
+
checkout(req: CheckoutRequest): Promise<CheckoutResponse>;
|
|
233
|
+
private onRetry;
|
|
234
|
+
private run;
|
|
235
|
+
private emitResult;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The API responded with a non-2xx status and a parsed `detail.error` body.
|
|
240
|
+
*
|
|
241
|
+
* `code` is one of the documented error codes in `contract/conformance.yaml`
|
|
242
|
+
* (e.g. `license_inactive`, `origin_not_allowed`, `rate_limit_exceeded`).
|
|
243
|
+
* Branch on `code`, not on `message` — the message is for humans and may
|
|
244
|
+
* change without notice.
|
|
245
|
+
*/
|
|
246
|
+
declare class LicensrApiError extends Error {
|
|
247
|
+
readonly status: number;
|
|
248
|
+
readonly code: LicensrErrorCode;
|
|
249
|
+
/** Present on 429 responses when the server sends `Retry-After`. */
|
|
250
|
+
readonly retryAfterSeconds: number | null;
|
|
251
|
+
constructor(status: number, code: LicensrErrorCode, message: string, retryAfterSeconds?: number | null);
|
|
252
|
+
}
|
|
253
|
+
/** The request never got a response — network failure, timeout, or abort. */
|
|
254
|
+
declare class LicensrNetworkError extends Error {
|
|
255
|
+
readonly cause: unknown;
|
|
256
|
+
constructor(message: string, cause: unknown);
|
|
257
|
+
}
|
|
258
|
+
/** An offline token failed EdDSA verification, was malformed, or has expired. */
|
|
259
|
+
declare class LicensrTokenVerificationError extends Error {
|
|
260
|
+
constructor(message: string);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface VerifyOfflineTokenOptions {
|
|
264
|
+
/** Override the fetch used for the JWKS lookup — mainly for testing. */
|
|
265
|
+
fetchImpl?: typeof fetch;
|
|
266
|
+
/** Reject if the token's `iat`/`nbf` are further in the future than this many seconds (clock skew tolerance). Default 60. */
|
|
267
|
+
clockToleranceSeconds?: number;
|
|
268
|
+
}
|
|
269
|
+
/** Drop cached JWKS lookups — call after rotating a plugin's signing key in tests, or to force a fresh fetch. */
|
|
270
|
+
declare function clearJwksCache(): void;
|
|
271
|
+
/**
|
|
272
|
+
* Verify an offline license token (from {@link import('./client.js').LicensrClient.token})
|
|
273
|
+
* fully offline: no network call beyond fetching (and caching) the plugin's
|
|
274
|
+
* public JWKS. Throws {@link LicensrTokenVerificationError} on a bad
|
|
275
|
+
* signature, wrong algorithm, or an expired/not-yet-valid token.
|
|
276
|
+
*
|
|
277
|
+
* Tokens carry no revocation signal — this only proves the token was
|
|
278
|
+
* genuinely issued by Licensr and hasn't expired, not that the license is
|
|
279
|
+
* still active right now. Re-validate online (`validate()`/`token()`)
|
|
280
|
+
* before the token's `expiresAt`.
|
|
281
|
+
*/
|
|
282
|
+
declare function verifyOfflineToken(token: string, jwksUrl: string, options?: VerifyOfflineTokenOptions): Promise<OfflineTokenClaims>;
|
|
283
|
+
interface CachedOfflineToken {
|
|
284
|
+
token: string;
|
|
285
|
+
claims: OfflineTokenClaims;
|
|
286
|
+
/** `Date.now()` at the time this entry was cached. */
|
|
287
|
+
cachedAtMs: number;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Pluggable persistence for the last-known-good offline token per license,
|
|
291
|
+
* so an app can boot fully offline (no network at all) before its first
|
|
292
|
+
* successful `/token` call in a given session. The default
|
|
293
|
+
* {@link InMemoryOfflineTokenStore} does not persist across process
|
|
294
|
+
* restarts — provide your own (backed by `localStorage`, a config file,
|
|
295
|
+
* OS keychain, etc.) for that.
|
|
296
|
+
*/
|
|
297
|
+
interface OfflineTokenStore {
|
|
298
|
+
get(licenseKey: string): CachedOfflineToken | null | Promise<CachedOfflineToken | null>;
|
|
299
|
+
set(licenseKey: string, entry: CachedOfflineToken): void | Promise<void>;
|
|
300
|
+
clear(licenseKey: string): void | Promise<void>;
|
|
301
|
+
}
|
|
302
|
+
declare class InMemoryOfflineTokenStore implements OfflineTokenStore {
|
|
303
|
+
private readonly entries;
|
|
304
|
+
get(licenseKey: string): CachedOfflineToken | null;
|
|
305
|
+
set(licenseKey: string, entry: CachedOfflineToken): void;
|
|
306
|
+
clear(licenseKey: string): void;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export { type ActivateRequest, type ActivateResponse, type ActivationListItem, type ActivationType, type ActivationsRequest, type ActivationsResponse, type CachedOfflineToken, type CheckoutRequest, type CheckoutResponse, DEVICE_ID_HEADER, type DeactivateRequest, type DeactivateResponse, type Entitlement, type ErrorBody, type HttpClientConfig, InMemoryOfflineTokenStore, type LicenseStatus, LicensrApiError, LicensrClient, type LicensrClientConfig, type LicensrErrorCode, LicensrEventEmitter, type LicensrEventMap, LicensrNetworkError, LicensrTokenVerificationError, type OfflineTokenClaims, type OfflineTokenStore, type RetryOptions, type TokenResponse, type ValidateRequest, type ValidateResponse, type VerifyOfflineTokenOptions, clearJwksCache, verifyOfflineToken };
|