@sidub-inc/licensing-client 1.5.170 → 1.6.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 +101 -13
- package/dist/index.cjs +208 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +178 -98
- package/dist/index.esm.js +209 -169
- package/dist/index.esm.js.map +1 -1
- package/docs/MIGRATION.md +122 -1
- package/docs/REACT_GUIDE.md +57 -10
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -54,13 +54,24 @@ const client = new LicensingClient({
|
|
|
54
54
|
| `serviceKeyPublicMember` | No | Base64 EC public key (P-256 SPKI) for signature verification |
|
|
55
55
|
| `consumptionServiceUri` | No | Separate URI for consumption reporting |
|
|
56
56
|
| `timeout` | No | Request timeout in ms (default: 30000) |
|
|
57
|
-
| `validateSignatures` | No |
|
|
57
|
+
| `validateSignatures` | No | Signature validation posture. Unset = auto (on when a verification key is configured; a one-time warning is emitted when it ends up off). `true` without a key throws `LicensingConfigurationException`. `false` = explicit unsigned mode, no warning. |
|
|
58
58
|
| `cacheEnabled` | No | Enable authorization caching (default: true) |
|
|
59
59
|
| `cacheMaxSize` | No | Maximum cached authorizations (default: 100) |
|
|
60
|
+
| `cacheFreshTtlMs` | No | Upper bound in ms on cache freshness before revalidation (default: 24h cap; 1h for no-expiry authorizations). Revocation latency = min(`cacheFreshTtlMs`, remaining license window) — tighten for faster revocation pickup, at the cost of more calls. `0` revalidates every read. |
|
|
61
|
+
| `staleGraceMs` | No | Stale-serve grace window in ms: how long an expired-but-signature-validated cached authorization may still be served (flagged `isStale`) when the service is transiently unreachable (default: 24h, capped 7d, `0` disables). Denials (401/403/404) are never stale-served. |
|
|
60
62
|
| `contextProvider` | No | Custom `ILicensingContextProvider` for credential resolution |
|
|
61
63
|
| `billableResourceId` | No | Billable resource ID for consumption reporting |
|
|
62
64
|
| `billablePlanId` | No | Billable plan ID for consumption reporting |
|
|
63
65
|
|
|
66
|
+
### Authentication Channel
|
|
67
|
+
|
|
68
|
+
Every credentialed request sends the API key **header-only**: the branded
|
|
69
|
+
`dub-apiKey` header (the gateway's configured subscription-key name) plus the
|
|
70
|
+
platform-default `Ocp-Apim-Subscription-Key` header, exactly mirroring the .NET
|
|
71
|
+
SDK. The key is never placed in the URL. (Before 2.0 the SDK also appended a
|
|
72
|
+
`?subscription-key=` query string — removed because URL-borne secrets leak into
|
|
73
|
+
browser history, Referer headers, and proxy/CDN logs.)
|
|
74
|
+
|
|
64
75
|
## Core Operations
|
|
65
76
|
|
|
66
77
|
### 1. Get License Authorization
|
|
@@ -92,7 +103,7 @@ const hasAccess = client.assertLicense(assertion, authorization);
|
|
|
92
103
|
```typescript
|
|
93
104
|
await client.performOperation({
|
|
94
105
|
licenseId: 'license-uuid',
|
|
95
|
-
feature: {
|
|
106
|
+
feature: { featureKey: 'api-calls' },
|
|
96
107
|
operationType: 'increment',
|
|
97
108
|
quantity: 1,
|
|
98
109
|
timestamp: new Date()
|
|
@@ -134,6 +145,21 @@ const result = await client.pollCheckoutResult(session.sessionId, {
|
|
|
134
145
|
// Throws LicensingError if max attempts exceeded or signal aborted
|
|
135
146
|
```
|
|
136
147
|
|
|
148
|
+
**`'pending'` covers async settlement.** The server deliberately reports its
|
|
149
|
+
internal PaymentPending state (delayed-notification payment methods such as
|
|
150
|
+
bank debits) as `'pending'` on the wire. A session paid by bank debit can stay
|
|
151
|
+
`'pending'` for days and still complete — the result remains pollable for up to
|
|
152
|
+
14 days. Treat a prolonged `'pending'` as "money may still be moving", not as
|
|
153
|
+
failure.
|
|
154
|
+
|
|
155
|
+
**Store the credential out of JavaScript's reach.** `encodedCredential` is
|
|
156
|
+
base64-obfuscated, NOT encrypted — it contains the tenant's `apiAccessKey`.
|
|
157
|
+
Hand it to your backend for storage; do not persist it in `localStorage` or
|
|
158
|
+
anywhere page JavaScript (and therefore any XSS) can read it. The SDK itself
|
|
159
|
+
never writes the credential or API key to `localStorage` — it persists only
|
|
160
|
+
authorization payloads and consumption counters, and the API key appears in
|
|
161
|
+
storage key names only as a non-reversible hash.
|
|
162
|
+
|
|
137
163
|
### 5. Report Access Check (Telemetry)
|
|
138
164
|
|
|
139
165
|
Telemetry-only: the event is always sent non-billable with a zero amount. Billable
|
|
@@ -147,14 +173,19 @@ await client.reportAccessCheck('license-uuid', 'api-calls'); // explicit license
|
|
|
147
173
|
|
|
148
174
|
## Authorization Caching
|
|
149
175
|
|
|
150
|
-
Authorization responses are cached in memory
|
|
176
|
+
Authorization responses are cached (in memory, persisted to `localStorage` in
|
|
177
|
+
browsers — the raw API key is never stored, and appears in storage key names
|
|
178
|
+
only as a non-reversible hash) with TTL-based freshness. Caching is enabled by
|
|
179
|
+
default.
|
|
151
180
|
|
|
152
181
|
```typescript
|
|
153
182
|
const client = new LicensingClient({
|
|
154
183
|
licenseServiceUri: '...',
|
|
155
184
|
encodedCredential: '...',
|
|
156
|
-
cacheEnabled: true,
|
|
157
|
-
cacheMaxSize: 100
|
|
185
|
+
cacheEnabled: true, // default
|
|
186
|
+
cacheMaxSize: 100, // default
|
|
187
|
+
cacheFreshTtlMs: 3600000, // optional: tighten revalidation cadence (default cap: 24h)
|
|
188
|
+
staleGraceMs: 86400000 // optional: stale-serve window on outages (default: 24h)
|
|
158
189
|
});
|
|
159
190
|
|
|
160
191
|
// Manual cache management
|
|
@@ -162,7 +193,14 @@ client.clearCache(); // Remove all cached authorizations
|
|
|
162
193
|
client.invalidateCache('license-uuid'); // Remove entries for a specific license
|
|
163
194
|
```
|
|
164
195
|
|
|
165
|
-
When caching is enabled, repeated calls to `getAuthorization()` with the same
|
|
196
|
+
When caching is enabled, repeated calls to `getAuthorization()` with the same
|
|
197
|
+
license ID return the cached result until its freshness deadline —
|
|
198
|
+
min(license expiry, `cacheFreshTtlMs`, 24h; 1h when the authorization has no
|
|
199
|
+
expiry). **Revocation trade-off:** a revoked or downgraded license keeps
|
|
200
|
+
authorizing from cache until that deadline; tighten `cacheFreshTtlMs` to
|
|
201
|
+
shorten the window. During transient outages an expired-but-validated entry
|
|
202
|
+
may additionally be served flagged `isStale` for up to `staleGraceMs` — but a
|
|
203
|
+
server denial (401/403/404) is never masked.
|
|
166
204
|
|
|
167
205
|
## Context Providers
|
|
168
206
|
|
|
@@ -267,7 +305,7 @@ Rate-limit assertions do NOT return `false` when the limit is exceeded — they
|
|
|
267
305
|
import { RateLimitAssertion, RateLimitError } from '@sidub-inc/licensing-client';
|
|
268
306
|
|
|
269
307
|
const assertion = RateLimitAssertion.create({
|
|
270
|
-
|
|
308
|
+
featureKey: 'api-calls',
|
|
271
309
|
rateLimit: 1000,
|
|
272
310
|
currentConsumption: 500
|
|
273
311
|
});
|
|
@@ -446,7 +484,22 @@ When `serviceKeyId` and `serviceKeyPublicMember` are configured (directly or via
|
|
|
446
484
|
3. Client verifies signature using configured public key
|
|
447
485
|
4. Invalid signatures throw `CryptoError`
|
|
448
486
|
|
|
449
|
-
**
|
|
487
|
+
**The posture is always explicit (2.0):**
|
|
488
|
+
|
|
489
|
+
- **Keys configured** (the recommended `encodedCredential` setup carries them):
|
|
490
|
+
validation is on automatically.
|
|
491
|
+
- **No keys and `validateSignatures` unset** (e.g. an apiKey-only config):
|
|
492
|
+
validation is OFF and the client emits a one-time console warning — the
|
|
493
|
+
authorization body is accepted without cryptographic verification.
|
|
494
|
+
- **`validateSignatures: true` with no keys**: `getAuthorization()` throws
|
|
495
|
+
`LicensingConfigurationException` — you demanded validation that cannot
|
|
496
|
+
happen.
|
|
497
|
+
- **`validateSignatures: false`**: explicit unsigned mode; no warning.
|
|
498
|
+
|
|
499
|
+
Check the effective posture at runtime via `client.isSignatureValidationEnabled`
|
|
500
|
+
(also surfaced by the `useLicensingContextValue()` hook).
|
|
501
|
+
|
|
502
|
+
**Disable validation explicitly (not recommended):**
|
|
450
503
|
```typescript
|
|
451
504
|
const client = new LicensingClient({
|
|
452
505
|
licenseServiceUri: '...',
|
|
@@ -475,11 +528,30 @@ const decoded = decodeCredential(encoded); // LicensingCredential
|
|
|
475
528
|
|
|
476
529
|
| Error Type | Description |
|
|
477
530
|
|-----------|-------------|
|
|
478
|
-
| `LicensingError` | Base error for API, network, and timeout errors |
|
|
531
|
+
| `LicensingError` | Base error for API, network, and timeout errors — carries the server's error contract (see below) |
|
|
479
532
|
| `LicensingConfigurationException` | Missing required configuration fields (extends `LicensingError`) |
|
|
480
533
|
| `CryptoError` | Signature validation failures (`KEY_IMPORT_FAILED`, `VERIFICATION_FAILED`, `INVALID_SIGNATURE`) |
|
|
481
534
|
| `RateLimitError` | Thrown by `assertLicense` when a `RateLimitAssertion` limit is exceeded — catch it; it is the enforcement signal, not a failure |
|
|
482
535
|
|
|
536
|
+
### The server error contract
|
|
537
|
+
|
|
538
|
+
On every HTTP failure the licensing service returns a structured `ApiError`
|
|
539
|
+
body — `{Code, Message, CorrelationId, Details[]}` — plus an `X-Correlation-Id`
|
|
540
|
+
header. `LicensingError` parses it (2.0):
|
|
541
|
+
|
|
542
|
+
| Field | Description |
|
|
543
|
+
|-------|-------------|
|
|
544
|
+
| `code` | Machine-readable server code (`'ApiKeyMissing'`, `'ApiKeyInvalid'`, `'AuthorizationDenied'`, `'NotFound'`, `'ValidationError'`, `'ServerError'`, ...) — branch on this, not on message text |
|
|
545
|
+
| `correlationId` | The request correlation id (body or `X-Correlation-Id` header) — quote it in support requests |
|
|
546
|
+
| `details` | Optional per-field entries `{code, message, field, targetId}` |
|
|
547
|
+
| `message` | Prefers the server's human-readable `Message`; falls back to the HTTP status text |
|
|
548
|
+
| `statusCode` | HTTP status code |
|
|
549
|
+
| `response` | The raw response body text (always preserved) |
|
|
550
|
+
| `retryAfterMs` | Parsed `Retry-After` header, when the server supplied one |
|
|
551
|
+
|
|
552
|
+
`code` and `correlationId` are undefined only when the failure never reached
|
|
553
|
+
the server (network error, timeout) or the server did not supply them.
|
|
554
|
+
|
|
483
555
|
```typescript
|
|
484
556
|
import { LicensingError, LicensingConfigurationException, CryptoError } from '@sidub-inc/licensing-client';
|
|
485
557
|
|
|
@@ -493,8 +565,15 @@ try {
|
|
|
493
565
|
// Missing configuration
|
|
494
566
|
console.error('Config error:', error.message);
|
|
495
567
|
} else if (error instanceof LicensingError) {
|
|
496
|
-
// API or network error
|
|
497
|
-
|
|
568
|
+
// API or network error — branch on the machine code
|
|
569
|
+
if (error.code === 'AuthorizationDenied') {
|
|
570
|
+
showUpgradePrompt();
|
|
571
|
+
} else {
|
|
572
|
+
console.error(
|
|
573
|
+
`Licensing error ${error.code ?? error.statusCode}: ${error.message}`,
|
|
574
|
+
`correlationId=${error.correlationId ?? 'n/a'}`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
498
577
|
}
|
|
499
578
|
}
|
|
500
579
|
```
|
|
@@ -512,9 +591,18 @@ import type {
|
|
|
512
591
|
} from '@sidub-inc/licensing-client';
|
|
513
592
|
```
|
|
514
593
|
|
|
515
|
-
## Migrating
|
|
594
|
+
## Migrating
|
|
595
|
+
|
|
596
|
+
See [MIGRATION.md](docs/MIGRATION.md) for detailed upgrade instructions.
|
|
597
|
+
|
|
598
|
+
**1.x → 2.0 (breaking):**
|
|
599
|
+
- `featureId` → `featureKey` on all feature models
|
|
600
|
+
- Credential is header-only (`dub-apiKey` + `Ocp-Apim-Subscription-Key`); the `?subscription-key=` query string and the dead `dub-issuerKey` header are gone
|
|
601
|
+
- `validateSignatures: true` without a verification key now throws
|
|
602
|
+
- `SignatureValidator.validateRawResponse` removed — use `validateResponseText`
|
|
603
|
+
- Low-level storage helpers (`save`/`load`/...) no longer exported
|
|
516
604
|
|
|
517
|
-
|
|
605
|
+
**1.0.x → 1.1:**
|
|
518
606
|
- `BillingIntervalUnit` enum members renamed: `Days`→`Day`, `Months`→`Month`, `Years`→`Year` (+ new `Hour`, `Week`)
|
|
519
607
|
- `performOperation()` payload structure changed (nested `LicenseOperation`)
|
|
520
608
|
- Configuration validation now throws `LicensingConfigurationException` instead of `LicensingError`
|