@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 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.