@sidub-inc/licensing-client 1.3.43
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 +559 -0
- package/dist/index.d.ts +1058 -0
- package/dist/index.esm.js +1657 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1689 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
# @sidub-inc/licensing-client
|
|
2
|
+
|
|
3
|
+
TypeScript/React runtime library for Sidub Licensing. Provides license enforcement, feature gating, consumption reporting, and cryptographic signature verification.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sidub-inc/licensing-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Requirements:**
|
|
12
|
+
- Modern browsers with Web Crypto API support (Chrome, Firefox, Safari, Edge)
|
|
13
|
+
- Node.js 18+ (or polyfill for `fetch` and `crypto.subtle`)
|
|
14
|
+
- React 16.8+ (optional, for hooks)
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
### Using Encoded Credentials (Recommended)
|
|
19
|
+
|
|
20
|
+
Encoded credentials bundle all required values into a single portable string:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { LicensingClient } from '@sidub-inc/licensing-client';
|
|
24
|
+
|
|
25
|
+
const client = new LicensingClient({
|
|
26
|
+
licenseServiceUri: 'https://api.monaiq.com/licensing',
|
|
27
|
+
encodedCredential: 'SIDUB_LIC_eyJ...' // From your license portal
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The encoded credential contains: `licenseId`, `serviceKeyId`, `serviceKeyPublicMember`, and `apiAccessKey`.
|
|
32
|
+
|
|
33
|
+
### Manual Configuration
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const client = new LicensingClient({
|
|
37
|
+
licenseServiceUri: 'https://api.monaiq.com/licensing',
|
|
38
|
+
apiKey: 'your-api-key',
|
|
39
|
+
licenseId: 'your-license-uuid', // Optional if passed to getAuthorization()
|
|
40
|
+
serviceKeyId: 'your-service-key-uuid', // Required for signature validation
|
|
41
|
+
serviceKeyPublicMember: 'base64-encoded-public-key' // Required for signature validation
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Configuration Reference
|
|
46
|
+
|
|
47
|
+
| Property | Required | Description |
|
|
48
|
+
|----------|----------|-------------|
|
|
49
|
+
| `licenseServiceUri` | Yes | Base URI for the licensing API |
|
|
50
|
+
| `encodedCredential` | No | Encoded credential string (SIDUB_LIC_...) |
|
|
51
|
+
| `apiKey` | No | API authentication key |
|
|
52
|
+
| `licenseId` | No | Default license ID |
|
|
53
|
+
| `serviceKeyId` | No | Key ID for signature verification |
|
|
54
|
+
| `serviceKeyPublicMember` | No | Base64 EC public key (P-256 SPKI) for signature verification |
|
|
55
|
+
| `consumptionServiceUri` | No | Separate URI for consumption reporting |
|
|
56
|
+
| `timeout` | No | Request timeout in ms (default: 30000) |
|
|
57
|
+
| `validateSignatures` | No | Enable/disable signature validation (default: auto) |
|
|
58
|
+
| `cacheEnabled` | No | Enable authorization caching (default: true) |
|
|
59
|
+
| `cacheMaxSize` | No | Maximum cached authorizations (default: 100) |
|
|
60
|
+
| `contextProvider` | No | Custom `ILicensingContextProvider` for credential resolution |
|
|
61
|
+
| `billableResourceId` | No | Billable resource ID for consumption reporting |
|
|
62
|
+
| `billablePlanId` | No | Billable plan ID for consumption reporting |
|
|
63
|
+
|
|
64
|
+
## Core Operations
|
|
65
|
+
|
|
66
|
+
### 1. Get License Authorization
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// Using configured license ID
|
|
70
|
+
const authorization = await client.getAuthorization();
|
|
71
|
+
|
|
72
|
+
// With explicit license ID
|
|
73
|
+
const authorization = await client.getAuthorization('license-uuid');
|
|
74
|
+
|
|
75
|
+
// Authorization includes:
|
|
76
|
+
// - licenseId, classification, features[], issuedAt, expiresAt
|
|
77
|
+
// - signature (if server-signed)
|
|
78
|
+
// - signatureValidated (true if cryptographic verification passed)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Assert License Conditions
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { ServiceAccessAssertion, ServiceAccessLevel } from '@sidub-inc/licensing-client';
|
|
85
|
+
|
|
86
|
+
const assertion = ServiceAccessAssertion.create('premium-feature', ServiceAccessLevel.Allowed);
|
|
87
|
+
const hasAccess = client.assertLicense(assertion, authorization);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Report Consumption
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await client.performOperation({
|
|
94
|
+
licenseId: 'license-uuid',
|
|
95
|
+
feature: { featureId: 'api-calls' },
|
|
96
|
+
operationType: 'increment',
|
|
97
|
+
quantity: 1,
|
|
98
|
+
timestamp: new Date()
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 4. Checkout Flow
|
|
103
|
+
|
|
104
|
+
Create embedded checkout sessions for purchasing offerings:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Create a checkout session
|
|
108
|
+
const session = await client.createCheckoutSession({
|
|
109
|
+
offeringId: 'offering-uuid',
|
|
110
|
+
issuerClientId: 'issuer-uuid',
|
|
111
|
+
correlationId: 'correlation-uuid',
|
|
112
|
+
customerEmail: 'user@example.com',
|
|
113
|
+
successUrl: 'https://example.com/success',
|
|
114
|
+
cancelUrl: 'https://example.com/cancel'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// For paid offerings, redirect to payment
|
|
118
|
+
if (session.sessionUrl) {
|
|
119
|
+
window.location.href = session.sessionUrl;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Poll for checkout result
|
|
123
|
+
const result = await client.getCheckoutResult(session.sessionId);
|
|
124
|
+
// result.status: 'pending' | 'completed' | 'failed'
|
|
125
|
+
// result.encodedCredential: SIDUB_LIC_... (on completion)
|
|
126
|
+
|
|
127
|
+
// Poll with exponential backoff (recommended for full lifecycle)
|
|
128
|
+
const result = await client.pollCheckoutResult(session.sessionId, {
|
|
129
|
+
maxAttempts: 60, // default: 60
|
|
130
|
+
signal: abortController.signal // optional: cancel polling
|
|
131
|
+
});
|
|
132
|
+
// result.status: 'completed' | 'failed'
|
|
133
|
+
// result.encodedCredential: SIDUB_LIC_... (on completion)
|
|
134
|
+
// Throws LicensingError if max attempts exceeded or signal aborted
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 5. Report Access Check (Telemetry)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
await client.reportAccessCheck(authorization, {
|
|
141
|
+
featureId: 'api-calls',
|
|
142
|
+
featureKey: 'api-calls',
|
|
143
|
+
IsBillable: true,
|
|
144
|
+
Amount: 1
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Authorization Caching
|
|
149
|
+
|
|
150
|
+
Authorization responses are cached in memory with TTL-based expiry (derived from `expiresAt`). Caching is enabled by default.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const client = new LicensingClient({
|
|
154
|
+
licenseServiceUri: '...',
|
|
155
|
+
encodedCredential: '...',
|
|
156
|
+
cacheEnabled: true, // default
|
|
157
|
+
cacheMaxSize: 100 // default
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Manual cache management
|
|
161
|
+
client.clearCache(); // Remove all cached authorizations
|
|
162
|
+
client.invalidateCache('license-uuid'); // Remove entries for a specific license
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When caching is enabled, repeated calls to `getAuthorization()` with the same license ID return the cached result until it expires.
|
|
166
|
+
|
|
167
|
+
## Context Providers
|
|
168
|
+
|
|
169
|
+
Context providers resolve licensing credentials dynamically, supporting multi-tenant scenarios.
|
|
170
|
+
|
|
171
|
+
### ConfigurationContextProvider (Default)
|
|
172
|
+
|
|
173
|
+
Builds context from `LicensingConfig` fields. Used automatically when no custom provider is configured:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { ConfigurationContextProvider } from '@sidub-inc/licensing-client';
|
|
177
|
+
|
|
178
|
+
const provider = new ConfigurationContextProvider(config);
|
|
179
|
+
const context = await provider.resolveContext();
|
|
180
|
+
// context: { licenseId, serviceKeyId, serviceKeyPublicMember, apiAccessKey, billableResourceId?, billablePlanId? }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Custom Context Provider
|
|
184
|
+
|
|
185
|
+
Implement `ILicensingContextProvider` for custom resolution strategies (e.g., per-tenant lookup):
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { ILicensingContextProvider, LicensingContextType } from '@sidub-inc/licensing-client';
|
|
189
|
+
|
|
190
|
+
class TenantContextProvider implements ILicensingContextProvider {
|
|
191
|
+
async resolveContext(): Promise<LicensingContextType | null> {
|
|
192
|
+
const tenantCreds = await fetchTenantCredentials();
|
|
193
|
+
if (!tenantCreds) return null;
|
|
194
|
+
return {
|
|
195
|
+
licenseId: tenantCreds.licenseId,
|
|
196
|
+
serviceKeyId: tenantCreds.serviceKeyId,
|
|
197
|
+
serviceKeyPublicMember: tenantCreds.publicKey,
|
|
198
|
+
apiAccessKey: tenantCreds.apiKey
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Portable Encoded Credentials
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { licensingContextFromEncodedString, licensingContextToEncodedString } from '@sidub-inc/licensing-client';
|
|
208
|
+
|
|
209
|
+
// Decode an encoded credential string into a context
|
|
210
|
+
const context = licensingContextFromEncodedString('SIDUB_LIC_eyJ...', billableResourceId, billablePlanId);
|
|
211
|
+
|
|
212
|
+
// Encode a context back to a portable string (billable fields are not included)
|
|
213
|
+
const encoded = licensingContextToEncodedString(context);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Feature State (Rate Limiting)
|
|
217
|
+
|
|
218
|
+
Track local rate-limit consumption within time windows:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { RateLimitFeatureState } from '@sidub-inc/licensing-client';
|
|
222
|
+
|
|
223
|
+
// Track consumption within a 60-second window
|
|
224
|
+
const state = new RateLimitFeatureState(60);
|
|
225
|
+
state.consumeRate(1);
|
|
226
|
+
state.consumeRate(1);
|
|
227
|
+
const current = state.getConsumption(); // 2 (entries older than 60s are pruned)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`RateLimitAssertion` consults local feature state before server-reported consumption:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// Store feature state on the client
|
|
234
|
+
client.setFeatureState('api-calls', state);
|
|
235
|
+
const stored = client.getFeatureState('api-calls');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Assertions
|
|
239
|
+
|
|
240
|
+
Assertions provide declarative license condition checking.
|
|
241
|
+
|
|
242
|
+
### FeatureExistsAssertion
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { FeatureExistsAssertion } from '@sidub-inc/licensing-client';
|
|
246
|
+
|
|
247
|
+
const assertion = FeatureExistsAssertion.create('analytics');
|
|
248
|
+
const hasFeature = client.assertLicense(assertion, authorization);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### ServiceAccessAssertion
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { ServiceAccessAssertion, ServiceAccessLevel } from '@sidub-inc/licensing-client';
|
|
255
|
+
|
|
256
|
+
const assertion = ServiceAccessAssertion.create('api-access', ServiceAccessLevel.Allowed);
|
|
257
|
+
const hasAccess = client.assertLicense(assertion, authorization);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### RateLimitAssertion
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { RateLimitAssertion } from '@sidub-inc/licensing-client';
|
|
264
|
+
|
|
265
|
+
const assertion = RateLimitAssertion.create({
|
|
266
|
+
featureId: 'api-calls',
|
|
267
|
+
rateLimit: 1000,
|
|
268
|
+
currentConsumption: 500
|
|
269
|
+
});
|
|
270
|
+
const withinLimit = client.assertLicense(assertion, authorization);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Composite Assertions
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { CompositeAssertion, FeatureExistsAssertion } from '@sidub-inc/licensing-client';
|
|
277
|
+
|
|
278
|
+
// AND: all must be satisfied
|
|
279
|
+
const both = CompositeAssertion.and(
|
|
280
|
+
FeatureExistsAssertion.create('feature1'),
|
|
281
|
+
FeatureExistsAssertion.create('feature2')
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// OR: any must be satisfied
|
|
285
|
+
const either = CompositeAssertion.or(
|
|
286
|
+
FeatureExistsAssertion.create('feature1'),
|
|
287
|
+
FeatureExistsAssertion.create('feature2')
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// NOT: invert result
|
|
291
|
+
import { NotAssertion } from '@sidub-inc/licensing-client';
|
|
292
|
+
const notBlocked = NotAssertion.create(FeatureExistsAssertion.create('blocked'));
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## React Integration
|
|
296
|
+
|
|
297
|
+
### Provider Setup
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { LicensingProvider } from '@sidub-inc/licensing-client';
|
|
301
|
+
|
|
302
|
+
function App() {
|
|
303
|
+
return (
|
|
304
|
+
<LicensingProvider
|
|
305
|
+
config={{
|
|
306
|
+
licenseServiceUri: 'https://api.monaiq.com/licensing',
|
|
307
|
+
encodedCredential: process.env.REACT_APP_LICENSE_CREDENTIAL
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
<YourApp />
|
|
311
|
+
</LicensingProvider>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### useLicenseAuthorization
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
import { useLicenseAuthorization } from '@sidub-inc/licensing-client';
|
|
320
|
+
|
|
321
|
+
function LicensedComponent() {
|
|
322
|
+
const {
|
|
323
|
+
authorization, // LicenseAuthorization | null
|
|
324
|
+
loading, // boolean
|
|
325
|
+
error, // LicensingError | null
|
|
326
|
+
signatureValidated, // boolean
|
|
327
|
+
fetchAuthorization, // (licenseId?: string) => Promise<LicenseAuthorization>
|
|
328
|
+
clearAuthorization // () => void
|
|
329
|
+
} = useLicenseAuthorization();
|
|
330
|
+
|
|
331
|
+
if (loading) return <div>Loading...</div>;
|
|
332
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
333
|
+
if (!authorization) return null;
|
|
334
|
+
|
|
335
|
+
return <div>License loaded {signatureValidated && '✓'}</div>;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### useAssertion
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { useLicenseAuthorization, useAssertion, ServiceAccessAssertion, ServiceAccessLevel } from '@sidub-inc/licensing-client';
|
|
343
|
+
|
|
344
|
+
function FeatureGate() {
|
|
345
|
+
const { authorization } = useLicenseAuthorization();
|
|
346
|
+
const assertion = ServiceAccessAssertion.create('premium', ServiceAccessLevel.Allowed);
|
|
347
|
+
const hasPremium = useAssertion(assertion, authorization);
|
|
348
|
+
|
|
349
|
+
return hasPremium ? <PremiumFeature /> : <UpgradePrompt />;
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### useLicenseFeature
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { useLicenseAuthorization, useLicenseFeature } from '@sidub-inc/licensing-client';
|
|
357
|
+
|
|
358
|
+
function Component() {
|
|
359
|
+
const { authorization } = useLicenseAuthorization();
|
|
360
|
+
const hasAnalytics = useLicenseFeature(authorization, 'analytics');
|
|
361
|
+
|
|
362
|
+
return hasAnalytics ? <Analytics /> : null;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### useLicenseValidity
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
import { useLicenseAuthorization, useLicenseValidity } from '@sidub-inc/licensing-client';
|
|
370
|
+
|
|
371
|
+
function LicenseStatus() {
|
|
372
|
+
const { authorization } = useLicenseAuthorization();
|
|
373
|
+
const isValid = useLicenseValidity(authorization);
|
|
374
|
+
|
|
375
|
+
return <span>{isValid ? 'Active' : 'Expired'}</span>;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### useReportConsumption
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
import { useLicensingContext, useReportConsumption } from '@sidub-inc/licensing-client';
|
|
383
|
+
|
|
384
|
+
function UsageTracker() {
|
|
385
|
+
const client = useLicensingContext();
|
|
386
|
+
const { reportConsumption, isReporting } = useReportConsumption(client);
|
|
387
|
+
|
|
388
|
+
const handleUse = async () => {
|
|
389
|
+
await reportConsumption({
|
|
390
|
+
licenseId: 'license-uuid',
|
|
391
|
+
feature: { featureId: 'api-calls' },
|
|
392
|
+
operationType: 'increment',
|
|
393
|
+
quantity: 1,
|
|
394
|
+
timestamp: new Date()
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return <button onClick={handleUse} disabled={isReporting}>Track Usage</button>;
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Checkout Lifecycle (pollCheckoutResult)
|
|
403
|
+
|
|
404
|
+
Use `pollCheckoutResult()` on `LicensingClient` for the full checkout lifecycle with exponential backoff:
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { useLicensingContext } from '@sidub-inc/licensing-client';
|
|
408
|
+
|
|
409
|
+
function PurchaseButton({ offeringId }: { offeringId: string }) {
|
|
410
|
+
const client = useLicensingContext();
|
|
411
|
+
const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
|
|
412
|
+
const [result, setResult] = useState<CheckoutSessionResult | null>(null);
|
|
413
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
414
|
+
|
|
415
|
+
const handlePurchase = async () => {
|
|
416
|
+
setStatus('processing');
|
|
417
|
+
try {
|
|
418
|
+
const session = await client.createCheckoutSession({
|
|
419
|
+
offeringId,
|
|
420
|
+
issuerClientId: 'issuer-uuid',
|
|
421
|
+
correlationId: crypto.randomUUID(),
|
|
422
|
+
customerEmail: 'user@example.com',
|
|
423
|
+
successUrl: window.location.origin + '/success',
|
|
424
|
+
cancelUrl: window.location.origin + '/cancel'
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// For paid offerings, redirect to payment
|
|
428
|
+
if (session.sessionUrl) {
|
|
429
|
+
window.location.href = session.sessionUrl;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// For free/trial offerings, poll for result
|
|
434
|
+
controllerRef.current = new AbortController();
|
|
435
|
+
const checkoutResult = await client.pollCheckoutResult(session.sessionId, {
|
|
436
|
+
signal: controllerRef.current.signal,
|
|
437
|
+
maxAttempts: 60 // ~15 min with exponential backoff (1s → 15s cap)
|
|
438
|
+
});
|
|
439
|
+
setResult(checkoutResult);
|
|
440
|
+
setStatus(checkoutResult.status === 'completed' ? 'completed' : 'failed');
|
|
441
|
+
} catch (error) {
|
|
442
|
+
setStatus('failed');
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
return () => controllerRef.current?.abort();
|
|
448
|
+
}, []);
|
|
449
|
+
|
|
450
|
+
if (status === 'completed') return <div>Purchase complete! License: {result?.licenseId}</div>;
|
|
451
|
+
if (status === 'failed') return <div>Error occurred <button onClick={() => setStatus('idle')}>Retry</button></div>;
|
|
452
|
+
|
|
453
|
+
return <button onClick={handlePurchase} disabled={status === 'processing'}>Purchase</button>;
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`pollCheckoutResult()` uses exponential backoff (1s initial, doubling to 15s cap, 60 max attempts ~15 min total). Pass an `AbortSignal` to cancel polling. Throws `LicensingError` on timeout or cancellation.
|
|
458
|
+
|
|
459
|
+
### Standalone Hook (without Provider)
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
import { useLicensing } from '@sidub-inc/licensing-client';
|
|
463
|
+
|
|
464
|
+
function Component() {
|
|
465
|
+
const client = useLicensing({
|
|
466
|
+
licenseServiceUri: 'https://api.monaiq.com/licensing',
|
|
467
|
+
encodedCredential: 'SIDUB_LIC_...'
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Use client directly
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Signature Validation
|
|
475
|
+
|
|
476
|
+
When `serviceKeyId` and `serviceKeyPublicMember` are configured (directly or via encoded credential), the library validates authorization signatures using ECDSA with P-256 (secp256r1) and SHA-256.
|
|
477
|
+
|
|
478
|
+
**Validation flow:**
|
|
479
|
+
1. Server signs `LicenseAuthorization` with private EC key (P-256)
|
|
480
|
+
2. Client receives authorization with `signature` field
|
|
481
|
+
3. Client verifies signature using configured public key
|
|
482
|
+
4. Invalid signatures throw `CryptoError`
|
|
483
|
+
|
|
484
|
+
**Disable validation (not recommended):**
|
|
485
|
+
```typescript
|
|
486
|
+
const client = new LicensingClient({
|
|
487
|
+
licenseServiceUri: '...',
|
|
488
|
+
encodedCredential: '...',
|
|
489
|
+
validateSignatures: false
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Credential Encoding
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { encodeCredential, decodeCredential, LicensingCredential } from '@sidub-inc/licensing-client';
|
|
497
|
+
|
|
498
|
+
const credential: LicensingCredential = {
|
|
499
|
+
licenseId: 'uuid',
|
|
500
|
+
serviceKeyId: 'uuid',
|
|
501
|
+
serviceKeyPublicMember: 'base64-key',
|
|
502
|
+
apiAccessKey: 'api-key'
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const encoded = encodeCredential(credential); // "SIDUB_LIC_eyJ..."
|
|
506
|
+
const decoded = decodeCredential(encoded); // LicensingCredential
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Error Handling
|
|
510
|
+
|
|
511
|
+
| Error Type | Description |
|
|
512
|
+
|-----------|-------------|
|
|
513
|
+
| `LicensingError` | Base error for API, network, and timeout errors |
|
|
514
|
+
| `LicensingConfigurationException` | Missing required configuration fields (extends `LicensingError`) |
|
|
515
|
+
| `CryptoError` | Signature validation failures (`KEY_IMPORT_FAILED`, `VERIFICATION_FAILED`, `INVALID_SIGNATURE`) |
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { LicensingError, LicensingConfigurationException, CryptoError } from '@sidub-inc/licensing-client';
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const authorization = await client.getAuthorization();
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof CryptoError) {
|
|
524
|
+
// Signature validation failed
|
|
525
|
+
console.error('Signature invalid:', error.code);
|
|
526
|
+
} else if (error instanceof LicensingConfigurationException) {
|
|
527
|
+
// Missing configuration
|
|
528
|
+
console.error('Config error:', error.message);
|
|
529
|
+
} else if (error instanceof LicensingError) {
|
|
530
|
+
// API or network error
|
|
531
|
+
console.error('Licensing error:', error.message, error.statusCode);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## TypeScript Types
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import type {
|
|
540
|
+
LicensingConfig,
|
|
541
|
+
LicenseAuthorization,
|
|
542
|
+
LicensingCredential,
|
|
543
|
+
ILicenseFeature,
|
|
544
|
+
LicenseClassificationType,
|
|
545
|
+
ILicenseAssertion
|
|
546
|
+
} from '@sidub-inc/licensing-client';
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Migrating from v1.0.x
|
|
550
|
+
|
|
551
|
+
See [MIGRATION.md](docs/MIGRATION.md) for detailed upgrade instructions. Key changes:
|
|
552
|
+
- `BillingIntervalUnit` enum members renamed: `Days`→`Day`, `Months`→`Month`, `Years`→`Year` (+ new `Hour`, `Week`)
|
|
553
|
+
- `performOperation()` payload structure changed (nested `LicenseOperation`)
|
|
554
|
+
- Configuration validation now throws `LicensingConfigurationException` instead of `LicensingError`
|
|
555
|
+
- Consumption requests use `dub-apiKey` header (was `dub-issuerKey`)
|
|
556
|
+
|
|
557
|
+
## License
|
|
558
|
+
|
|
559
|
+
MIT © Sidub Inc.
|