feat: license handshake and x-descope-license header#730
Conversation
Adds a management.license.get() endpoint that calls /v1/mgmt/license and returns the rate limit tier. The SDK fires the request once on init (when a managementKey is configured) and injects the returned tier value in the x-descope-license header on every subsequent management request so Cloudflare can apply the correct rate limit bucket per customer tier. Tier values: tier1 (free), tier2 (pro), tier3 (growth), tier4 (enterprise). Ref: descope/etc#14245
Fire-and-forget handshake and the rate-limit-tier header injection are best-effort defensive code paths. The license endpoint client itself remains fully covered by license.test.ts; the end-to-end handshake will be exercised by integration tests.
There was a problem hiding this comment.
Pull request overview
Adds a license-tier handshake to the management SDK so Cloudflare can apply per-customer rate-limit buckets. When a managementKey is configured, the SDK asynchronously fetches the customer's tier on initialization and injects it into every subsequent management request as the x-descope-license header.
Changes:
- New
management.license.get()endpoint (GET /v1/mgmt/license) andLicensetype withrateLimitTier. - On SDK init, fires a one-shot license fetch and caches
rateLimitTier; failure logs atdebugand is non-fatal. - Management
beforeRequesthook injectsx-descope-license: <tier>once cached.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/management/types.ts | Adds License type. |
| lib/management/paths.ts | Adds /v1/mgmt/license path entry. |
| lib/management/license.ts | New wrapper exposing get() for the license endpoint. |
| lib/management/license.test.ts | Unit test for the new get() endpoint. |
| lib/management/index.ts | Wires withLicense into the management surface. |
| lib/index.ts | Adds tier cache, fire-and-forget handshake, and header injection in beforeRequest. |
Comments suppressed due to low confidence (1)
lib/index.ts:174
- The
.catchhandler swallows handshake errors with only alogger?.debug?.call. Ifloggeris not provided, or itsdebuglevel is filtered out (which is common in production logging configurations), the failure becomes completely invisible — operators have no signal that the handshake is failing and the tier header is missing. Consider usinglogger?.warn(or at leastlogger?.info) for handshake failures since a missing tier header will cause Cloudflare to rate-limit the customer at the default/lowest bucket, which has real customer-visible consequences.
.catch((e) => {
logger?.debug?.('License handshake failed', e);
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Warn (not debug) on handshake failure so operators see when the tier header is missing - Remove istanbul ignore directives and cover the handshake paths with unit tests
asafshen
left a comment
There was a problem hiding this comment.
cool, I think we should not expose this API to customers, other than this it looks good 👍
| fga: WithFGA(client, fgaConfig), | ||
| descoper: withDescoper(client), | ||
| managementKey: withManagementKey(client), | ||
| license: withLicense(client), |
There was a problem hiding this comment.
do we want to expose this API to customers? - imho - no
| // Fire-and-forget license handshake. Backend skips license-header validation | ||
| // for the GetLicense endpoint itself, so this initial request is safe even | ||
| // before the tier is cached. | ||
| if (managementKey) { |
There was a problem hiding this comment.
is this only for mgmt actions?
also, don't we have mgmt actions that are not bound to mgmt key?
| // eslint-disable-next-line no-param-reassign | ||
| requestConfig.headers = { | ||
| ...requestConfig.headers, | ||
| 'x-descope-license': rateLimitTier, |
There was a problem hiding this comment.
just making sure the Descope deployment already allow this header (if not, requests will start to fail)
|
|
||
| // Rate limit tier from the license handshake. Populated asynchronously on init | ||
| // and injected into the x-descope-license header on every management request so | ||
| // Cloudflare can apply the correct rate limit bucket per customer tier. |
There was a problem hiding this comment.
nit, I don't think logs should mention CF specifically, its implementation details
I would also not mention the word "customer" tier but "project's company" tier
| .get() | ||
| .then((resp) => { | ||
| if (resp.ok && resp.data?.rateLimitTier) { | ||
| rateLimitTier = resp.data.rateLimitTier; |
There was a problem hiding this comment.
have we considered to wait with other requests until this one completed?
Summary
Adds license handshake support so Cloudflare can apply the correct rate-limit bucket per customer tier.
management.license.get()(GET/v1/mgmt/license) returning{ rateLimitTier: string }managementKeyis configured, the SDK fires a one-shot license fetch in the background and cachesrateLimitTierx-descope-license: <tier>via the existingbeforeRequesthookTier values
tier1(free) /tier2(pro) /tier3(growth) /tier4(enterprise)Notes
logger?.debugand the SDK continues without the headerGetLicenseitself, so the initial fetch is safe before the tier is cachedTest plan
lib/management/license.test.tscovers the new endpointRef: descope/etc#14245