@sidub-inc/licensing-client
v1.5.1
Published
React/TypeScript runtime library for Sidub Licensing - License enforcement and consumption reporting
Maintainers
Readme
@sidub-inc/licensing-client
TypeScript/React runtime library for Sidub Licensing. Provides license enforcement, feature gating, consumption reporting, and cryptographic signature verification.
Installation
npm install @sidub-inc/licensing-clientRequirements:
- Modern browsers with Web Crypto API support (Chrome, Firefox, Safari, Edge)
- Node.js 18+ (or polyfill for
fetchandcrypto.subtle) - React 16.8+ (optional, for hooks)
Configuration
Using Encoded Credentials (Recommended)
Encoded credentials bundle all required values into a single portable string:
import { LicensingClient } from '@sidub-inc/licensing-client';
const client = new LicensingClient({
licenseServiceUri: 'https://api.monaiq.com/licensing',
encodedCredential: 'SIDUB_LIC_eyJ...' // From your license portal
});The encoded credential contains: licenseId, serviceKeyId, serviceKeyPublicMember, and apiAccessKey.
Manual Configuration
const client = new LicensingClient({
licenseServiceUri: 'https://api.monaiq.com/licensing',
apiKey: 'your-api-key',
licenseId: 'your-license-uuid', // Optional if passed to getAuthorization()
serviceKeyId: 'your-service-key-uuid', // Required for signature validation
serviceKeyPublicMember: 'base64-encoded-public-key' // Required for signature validation
});Configuration Reference
| Property | Required | Description |
|----------|----------|-------------|
| licenseServiceUri | Yes | Base URI for the licensing API |
| encodedCredential | No | Encoded credential string (SIDUB_LIC_...) |
| apiKey | No | API authentication key |
| licenseId | No | Default license ID |
| serviceKeyId | No | Key ID for signature verification |
| serviceKeyPublicMember | No | Base64 EC public key (P-256 SPKI) for signature verification |
| consumptionServiceUri | No | Separate URI for consumption reporting |
| timeout | No | Request timeout in ms (default: 30000) |
| validateSignatures | No | Enable/disable signature validation (default: auto) |
| cacheEnabled | No | Enable authorization caching (default: true) |
| cacheMaxSize | No | Maximum cached authorizations (default: 100) |
| contextProvider | No | Custom ILicensingContextProvider for credential resolution |
| billableResourceId | No | Billable resource ID for consumption reporting |
| billablePlanId | No | Billable plan ID for consumption reporting |
Core Operations
1. Get License Authorization
// Using configured license ID
const authorization = await client.getAuthorization();
// With explicit license ID
const authorization = await client.getAuthorization('license-uuid');
// Authorization includes:
// - licenseId, classification, features[], issuedAt, expiresAt
// - signature (if server-signed)
// - signatureValidated (true if cryptographic verification passed)2. Assert License Conditions
import { ServiceAccessAssertion, ServiceAccessLevel } from '@sidub-inc/licensing-client';
const assertion = ServiceAccessAssertion.create('premium-feature', ServiceAccessLevel.Allowed);
const hasAccess = client.assertLicense(assertion, authorization);3. Report Consumption
await client.performOperation({
licenseId: 'license-uuid',
feature: { featureId: 'api-calls' },
operationType: 'increment',
quantity: 1,
timestamp: new Date()
});4. Checkout Flow
Create embedded checkout sessions for purchasing offerings:
// Create a checkout session
const session = await client.createCheckoutSession({
offeringId: 'offering-uuid',
issuerClientId: 'issuer-uuid',
correlationId: 'correlation-uuid',
customerEmail: '[email protected]',
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel'
});
// For paid offerings, redirect to payment
if (session.sessionUrl) {
window.location.href = session.sessionUrl;
}
// Poll for checkout result
const result = await client.getCheckoutResult(session.sessionId);
// result.status: 'pending' | 'completed' | 'failed'
// result.encodedCredential: SIDUB_LIC_... (on completion)
// Poll with exponential backoff (recommended for full lifecycle)
const result = await client.pollCheckoutResult(session.sessionId, {
maxAttempts: 60, // default: 60
signal: abortController.signal // optional: cancel polling
});
// result.status: 'completed' | 'failed'
// result.encodedCredential: SIDUB_LIC_... (on completion)
// Throws LicensingError if max attempts exceeded or signal aborted5. Report Access Check (Telemetry)
await client.reportAccessCheck(authorization, {
featureId: 'api-calls',
featureKey: 'api-calls',
IsBillable: true,
Amount: 1
});Authorization Caching
Authorization responses are cached in memory with TTL-based expiry (derived from expiresAt). Caching is enabled by default.
const client = new LicensingClient({
licenseServiceUri: '...',
encodedCredential: '...',
cacheEnabled: true, // default
cacheMaxSize: 100 // default
});
// Manual cache management
client.clearCache(); // Remove all cached authorizations
client.invalidateCache('license-uuid'); // Remove entries for a specific licenseWhen caching is enabled, repeated calls to getAuthorization() with the same license ID return the cached result until it expires.
Context Providers
Context providers resolve licensing credentials dynamically, supporting multi-tenant scenarios.
ConfigurationContextProvider (Default)
Builds context from LicensingConfig fields. Used automatically when no custom provider is configured:
import { ConfigurationContextProvider } from '@sidub-inc/licensing-client';
const provider = new ConfigurationContextProvider(config);
const context = await provider.resolveContext();
// context: { licenseId, serviceKeyId, serviceKeyPublicMember, apiAccessKey, billableResourceId?, billablePlanId? }Custom Context Provider
Implement ILicensingContextProvider for custom resolution strategies (e.g., per-tenant lookup):
import { ILicensingContextProvider, LicensingContextType } from '@sidub-inc/licensing-client';
class TenantContextProvider implements ILicensingContextProvider {
async resolveContext(): Promise<LicensingContextType | null> {
const tenantCreds = await fetchTenantCredentials();
if (!tenantCreds) return null;
return {
licenseId: tenantCreds.licenseId,
serviceKeyId: tenantCreds.serviceKeyId,
serviceKeyPublicMember: tenantCreds.publicKey,
apiAccessKey: tenantCreds.apiKey
};
}
}Portable Encoded Credentials
import { licensingContextFromEncodedString, licensingContextToEncodedString } from '@sidub-inc/licensing-client';
// Decode an encoded credential string into a context
const context = licensingContextFromEncodedString('SIDUB_LIC_eyJ...', billableResourceId, billablePlanId);
// Encode a context back to a portable string (billable fields are not included)
const encoded = licensingContextToEncodedString(context);Feature State (Rate Limiting)
Track local rate-limit consumption within time windows:
import { RateLimitFeatureState } from '@sidub-inc/licensing-client';
// Track consumption within a 60-second window
const state = new RateLimitFeatureState(60);
state.consumeRate(1);
state.consumeRate(1);
const current = state.getConsumption(); // 2 (entries older than 60s are pruned)RateLimitAssertion consults local feature state before server-reported consumption:
// Store feature state on the client
client.setFeatureState('api-calls', state);
const stored = client.getFeatureState('api-calls');Assertions
Assertions provide declarative license condition checking.
FeatureExistsAssertion
import { FeatureExistsAssertion } from '@sidub-inc/licensing-client';
const assertion = FeatureExistsAssertion.create('analytics');
const hasFeature = client.assertLicense(assertion, authorization);ServiceAccessAssertion
import { ServiceAccessAssertion, ServiceAccessLevel } from '@sidub-inc/licensing-client';
const assertion = ServiceAccessAssertion.create('api-access', ServiceAccessLevel.Allowed);
const hasAccess = client.assertLicense(assertion, authorization);RateLimitAssertion
import { RateLimitAssertion } from '@sidub-inc/licensing-client';
const assertion = RateLimitAssertion.create({
featureId: 'api-calls',
rateLimit: 1000,
currentConsumption: 500
});
const withinLimit = client.assertLicense(assertion, authorization);Composite Assertions
import { CompositeAssertion, FeatureExistsAssertion } from '@sidub-inc/licensing-client';
// AND: all must be satisfied
const both = CompositeAssertion.and(
FeatureExistsAssertion.create('feature1'),
FeatureExistsAssertion.create('feature2')
);
// OR: any must be satisfied
const either = CompositeAssertion.or(
FeatureExistsAssertion.create('feature1'),
FeatureExistsAssertion.create('feature2')
);
// NOT: invert result
import { NotAssertion } from '@sidub-inc/licensing-client';
const notBlocked = NotAssertion.create(FeatureExistsAssertion.create('blocked'));React Integration
Provider Setup
import { LicensingProvider } from '@sidub-inc/licensing-client';
function App() {
return (
<LicensingProvider
config={{
licenseServiceUri: 'https://api.monaiq.com/licensing',
encodedCredential: process.env.REACT_APP_LICENSE_CREDENTIAL
}}
>
<YourApp />
</LicensingProvider>
);
}useLicenseAuthorization
import { useLicenseAuthorization } from '@sidub-inc/licensing-client';
function LicensedComponent() {
const {
authorization, // LicenseAuthorization | null
loading, // boolean
error, // LicensingError | null
signatureValidated, // boolean
fetchAuthorization, // (licenseId?: string) => Promise<LicenseAuthorization>
clearAuthorization // () => void
} = useLicenseAuthorization();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!authorization) return null;
return <div>License loaded {signatureValidated && '✓'}</div>;
}useAssertion
import { useLicenseAuthorization, useAssertion, ServiceAccessAssertion, ServiceAccessLevel } from '@sidub-inc/licensing-client';
function FeatureGate() {
const { authorization } = useLicenseAuthorization();
const assertion = ServiceAccessAssertion.create('premium', ServiceAccessLevel.Allowed);
const hasPremium = useAssertion(assertion, authorization);
return hasPremium ? <PremiumFeature /> : <UpgradePrompt />;
}useLicenseFeature
import { useLicenseAuthorization, useLicenseFeature } from '@sidub-inc/licensing-client';
function Component() {
const { authorization } = useLicenseAuthorization();
const hasAnalytics = useLicenseFeature(authorization, 'analytics');
return hasAnalytics ? <Analytics /> : null;
}useLicenseValidity
import { useLicenseAuthorization, useLicenseValidity } from '@sidub-inc/licensing-client';
function LicenseStatus() {
const { authorization } = useLicenseAuthorization();
const isValid = useLicenseValidity(authorization);
return <span>{isValid ? 'Active' : 'Expired'}</span>;
}useReportConsumption
import { useLicensingContext, useReportConsumption } from '@sidub-inc/licensing-client';
function UsageTracker() {
const client = useLicensingContext();
const { reportConsumption, isReporting } = useReportConsumption(client);
const handleUse = async () => {
await reportConsumption({
licenseId: 'license-uuid',
feature: { featureId: 'api-calls' },
operationType: 'increment',
quantity: 1,
timestamp: new Date()
});
};
return <button onClick={handleUse} disabled={isReporting}>Track Usage</button>;
}Checkout Lifecycle (pollCheckoutResult)
Use pollCheckoutResult() on LicensingClient for the full checkout lifecycle with exponential backoff:
import { useLicensingContext } from '@sidub-inc/licensing-client';
function PurchaseButton({ offeringId }: { offeringId: string }) {
const client = useLicensingContext();
const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
const [result, setResult] = useState<CheckoutSessionResult | null>(null);
const controllerRef = useRef<AbortController | null>(null);
const handlePurchase = async () => {
setStatus('processing');
try {
const session = await client.createCheckoutSession({
offeringId,
issuerClientId: 'issuer-uuid',
correlationId: crypto.randomUUID(),
customerEmail: '[email protected]',
successUrl: window.location.origin + '/success',
cancelUrl: window.location.origin + '/cancel'
});
// For paid offerings, redirect to payment
if (session.sessionUrl) {
window.location.href = session.sessionUrl;
return;
}
// For free/trial offerings, poll for result
controllerRef.current = new AbortController();
const checkoutResult = await client.pollCheckoutResult(session.sessionId, {
signal: controllerRef.current.signal,
maxAttempts: 60 // ~15 min with exponential backoff (1s → 15s cap)
});
setResult(checkoutResult);
setStatus(checkoutResult.status === 'completed' ? 'completed' : 'failed');
} catch (error) {
setStatus('failed');
}
};
useEffect(() => {
return () => controllerRef.current?.abort();
}, []);
if (status === 'completed') return <div>Purchase complete! License: {result?.licenseId}</div>;
if (status === 'failed') return <div>Error occurred <button onClick={() => setStatus('idle')}>Retry</button></div>;
return <button onClick={handlePurchase} disabled={status === 'processing'}>Purchase</button>;
}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.
Standalone Hook (without Provider)
import { useLicensing } from '@sidub-inc/licensing-client';
function Component() {
const client = useLicensing({
licenseServiceUri: 'https://api.monaiq.com/licensing',
encodedCredential: 'SIDUB_LIC_...'
});
// Use client directly
}Signature Validation
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.
Validation flow:
- Server signs
LicenseAuthorizationwith private EC key (P-256) - Client receives authorization with
signaturefield - Client verifies signature using configured public key
- Invalid signatures throw
CryptoError
Disable validation (not recommended):
const client = new LicensingClient({
licenseServiceUri: '...',
encodedCredential: '...',
validateSignatures: false
});Credential Encoding
import { encodeCredential, decodeCredential, LicensingCredential } from '@sidub-inc/licensing-client';
const credential: LicensingCredential = {
licenseId: 'uuid',
serviceKeyId: 'uuid',
serviceKeyPublicMember: 'base64-key',
apiAccessKey: 'api-key'
};
const encoded = encodeCredential(credential); // "SIDUB_LIC_eyJ..."
const decoded = decodeCredential(encoded); // LicensingCredentialError Handling
| Error Type | Description |
|-----------|-------------|
| LicensingError | Base error for API, network, and timeout errors |
| LicensingConfigurationException | Missing required configuration fields (extends LicensingError) |
| CryptoError | Signature validation failures (KEY_IMPORT_FAILED, VERIFICATION_FAILED, INVALID_SIGNATURE) |
import { LicensingError, LicensingConfigurationException, CryptoError } from '@sidub-inc/licensing-client';
try {
const authorization = await client.getAuthorization();
} catch (error) {
if (error instanceof CryptoError) {
// Signature validation failed
console.error('Signature invalid:', error.code);
} else if (error instanceof LicensingConfigurationException) {
// Missing configuration
console.error('Config error:', error.message);
} else if (error instanceof LicensingError) {
// API or network error
console.error('Licensing error:', error.message, error.statusCode);
}
}TypeScript Types
import type {
LicensingConfig,
LicenseAuthorization,
LicensingCredential,
ILicenseFeature,
LicenseClassificationType,
ILicenseAssertion
} from '@sidub-inc/licensing-client';Migrating from v1.0.x
See MIGRATION.md for detailed upgrade instructions. Key changes:
BillingIntervalUnitenum members renamed:Days→Day,Months→Month,Years→Year(+ newHour,Week)performOperation()payload structure changed (nestedLicenseOperation)- Configuration validation now throws
LicensingConfigurationExceptioninstead ofLicensingError - Consumption requests use
dub-apiKeyheader (wasdub-issuerKey)
License
Proprietary licenses available at https://sidub.ca/ © Sidub Inc.
