Skip to content

feat(license): dual-read v1/v2 entitlements with discrepancy logging#6914

Open
PrestigePvP wants to merge 2 commits into
mainfrom
tre/platfor-449-dual-read-v1v2-discrepancy-logging
Open

feat(license): dual-read v1/v2 entitlements with discrepancy logging#6914
PrestigePvP wants to merge 2 commits into
mainfrom
tre/platfor-449-dual-read-v1v2-discrepancy-logging

Conversation

@PrestigePvP

@PrestigePvP PrestigePvP commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Context

Phase 2 of the License Server v2 migration. getPlan becomes the mode-aware read toggle driven by LICENSE_SERVER_V2_MODE (off | read-compare | on), so the cutover from shadow comparison to serving v2 is a config flip, not a code change. The read path uses the real licenseClient SDK so v2 is exercised against real traffic before anything depends on it.

  • off serves v1 (today's behavior). read-compare serves v1 and shadow-compares v2 in the background via the real SDK, logging discrepancies and emitting DataDog diff/error counters. on serves a v2 entitlement set projected into the TFeatureSet shape, so getPlan's callers are untouched and flipping read-compare -> on needs no code change.
  • Replaces the boolean LICENSE_SERVER_V2_ENABLED with the tri-state mode and migrates the v2 SDK + usage-metering gating onto it. The old env var is removed (it was inert and never enabled in prod).
  • Adds projectV2ToFeatureSet: a generic v2 -> TFeatureSet projection over a data-driven v1<->v2 feature-mapping registry covering the full TFeatureSet. For features v2 omits or returns null for, the free-tier default is kept (the read-compare bake validates v2 completeness before the flip).
  • The comparison is a fire-and-forget compareInBackground off the getPlan cache-miss with the already-resolved plan passed in (no re-fetch, no self-emit loop). Replaces the earlier BullMQ queue/worker.
  • compareEntitlements is pure and classifies each feature as match | mismatch | v2_missing | v1_absent | indeterminate (a present-but-null v2 value counts as missing; null caps normalize to "unlimited").
  • Registers license.feature / license.dual_read.kind in the InfisicalCore meter allowlist so the diff counter keeps its dimensions, and emits on Victor's low-cardinality infisicalCoreMeter (not the legacy self-host meter).

The v2 feature keys are best-effort snake_case and must be reconciled with the License Server v2 feature registry; a wrong key surfaces as a v2_missing diff, which is intended bake signal. Lossy/non-entitlement fields the bake does not validate (slug, tier, usage counts) are projected on a best-effort basis; usage counts continue to be computed from the DB.

Linear: https://linear.app/infisical/issue/PLATFOR-449

Steps to verify the change

  • cd backend && npm run test:unit -- src/services/license-client -> 40 passed (comparator incl. null-handling, v2->TFeatureSet projection, mapping-completeness over the full TFeatureSet, migrated SDK/metering tests)
  • lint + type-check clean for the changed files
  • LICENSE_SERVER_V2_MODE=read-compare against a v2 backend: trigger a cloud getPlan, confirm license-dual-read discrepancy logs + diff/error counters; LICENSE_SERVER_V2_MODE=on: confirm getPlan serves the v2-projected plan; off: neither path runs

Type

  • Feature
  • Breaking

Checklist

  • Title follows the conventional commit format
  • Tested locally
  • Updated docs (not needed)
  • Updated CLAUDE.md files (not needed)

@linear

linear Bot commented Jun 17, 2026

Copy link
Copy Markdown

PLATFOR-449

@infisical-review-police

Copy link
Copy Markdown

💬 Discussion in Slack: #pr-review-infisical-6914-feat-license-dual-read-v1-v2-entitlements-with-discrepa

Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b742f8a77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread backend/src/services/license-client/dual-read/dual-read-service.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a shadow/bake phase for the License Server v2 migration: a new LICENSE_SERVER_V2_MODE tri-state env var (off | read-compare | on) replaces the defunct boolean LICENSE_SERVER_V2_ENABLED, and a debounced BullMQ dual-read worker now runs in the background on every cloud getPlan cache miss to compare v1 vs v2 entitlements and emit DataDog diff/error counters.

  • Env/config: LICENSE_SERVER_V2_MODE added with off default; isLicenseDualReadEnabled computed for read-compare; usage metering and the new dual-read service both gate on this flag.
  • Dual-read pipeline: dualReadServiceFactory enqueues a deduplicated 60 s-delayed job per org; dualReadQueueFactory runs the BullMQ worker that fetches v2 entitlements, calls getPlan with skipDualReadEmit: true to prevent re-entrancy, and runs compareEntitlements against a data-driven FEATURE_MAPPINGS registry covering all TFeatureSet fields.
  • Comparator: Pure function classifying each feature as match / mismatch / v2_missing / v1_absent / indeterminate; null caps normalize to UNLIMITED; covered by 35 unit tests including mapping-completeness enforcement.

Confidence Score: 4/5

The dual-read shadow path is fully fire-and-forget and cannot affect request traffic; v1 continues to serve all reads in every mode. The main concern is that the entitlements client doesn't enforce HTTPS for its target URL the way the usage reporter does.

The core logic — debounced BullMQ job, loop-safety via skipDualReadEmit, the comparator, and the mapping registry — is well-designed and covered by unit tests. The two findings are: the entitlements backend creates its HTTP client without the HTTPS URL validation that the usage reporter enforces (creating a credential-exposure risk if the URL is misconfigured), and the getPlan/compareEntitlements calls in the worker sit outside any try/catch, so unexpected failures won't increment the error metric. Neither finding affects production plan serving.

license-client.ts (missing HTTPS guard in buildBackend) and dual-read-queue.ts (error metric gap for the plan-fetch and comparison steps).

Security Review

  • Bearer token sent over plaintext HTTP (potential) (license-client.ts): buildBackend constructs the entitlements client from LICENSE_SERVER_V2_URL without validating the URL scheme. buildUsageReporter in the same PR enforces https: before creating a reporter; buildBackend has no equivalent guard. If LICENSE_SERVER_V2_URL is misconfigured to an HTTP URL, LICENSE_SERVER_V2_SERVICE_KEY is transmitted in plaintext on every entitlement fetch. The redirect: \"manual\" option in the underlying fetch call prevents redirect-based SSRF, but does not protect against a non-HTTPS destination.

Important Files Changed

Filename Overview
backend/src/services/license-client/dual-read/dual-read-queue.ts New BullMQ worker that fetches v2 entitlements and compares them against the v1 plan; getPlan/compareEntitlements calls are outside the error-capture try/catch, so unexpected failures won't be recorded in the error counter metric.
backend/src/services/license-client/license-client.ts Migrates kill-switch from boolean LICENSE_SERVER_V2_ENABLED to tri-state LICENSE_SERVER_V2_MODE; exposes getEntitlements publicly; buildBackend lacks the HTTPS URL validation that buildUsageReporter enforces.
backend/src/services/license-client/dual-read/dual-read-service.ts Debounced emit service using BullMQ jobId deduplication; fire-and-forget with proper catch; looks correct.
backend/src/services/license-client/dual-read/entitlement-comparator.ts Pure comparator with correct truthy-object guard for v2Raw and UNLIMITED-XOR Indeterminate logic; all edge cases are covered by the unit tests.
backend/src/ee/services/license/license-service.ts Adds optional skipDualReadEmit flag to getPlan cache-miss path; emit is correctly scoped to Cloud instance type and guarded against re-entrancy.
backend/src/lib/config/env.ts Replaces boolean LICENSE_SERVER_V2_ENABLED with tri-state enum LICENSE_SERVER_V2_MODE; adds isLicenseDualReadEnabled computed property; clean migration.
backend/src/server/routes/index.ts Wires up dualReadServiceFactory and dualReadQueueFactory in the correct order; dualReadQueue.init() added before healthAlert.init(); clean integration.

Comments Outside Diff (1)

  1. backend/src/services/license-client/license-client.ts, line 19-33 (link)

    P2 security SSRF: missing HTTPS check before bearer-token transmission

    buildBackend passes LICENSE_SERVER_V2_URL directly to licenseServerBackend without validating that the scheme is https:. The LICENSE_SERVER_V2_SERVICE_KEY bearer token is then sent in every entitlement request. buildUsageReporter (changed in this same PR) already enforces HTTPS:

    if (parsedUrl.protocol !== "https:") {
      logger.warn("usage-reporter: LICENSE_SERVER_V2_URL must use https; …");
      return null;
    }
    

    read-compare is the first mode where this backend becomes live, so the gap is now exploitable: if LICENSE_SERVER_V2_URL is misconfigured to an HTTP endpoint (or the URL is changed at the infrastructure level), the service key is transmitted in plaintext. Apply the same URL-parse + protocol guard here that buildUsageReporter uses.

    Context Used: Flag SSRF risks (source)

Reviews (1): Last reviewed commit: "feat(license): dual-read v1/v2 entitleme..." | Re-trigger Greptile

Comment thread backend/src/services/license-client/dual-read/dual-read-queue.ts Outdated
PrestigePvP added a commit that referenced this pull request Jun 18, 2026
Addresses PR review feedback on #6914.

- BullMQ throws "Custom Id cannot contain :" for a custom jobId with a single
  colon, so the dual-read job never enqueued; switch to the hyphen jobId
  convention used everywhere else so compares actually run
- Wrap getEntitlements + getPlan + compareEntitlements in one try/catch so every
  failure increments infisical.license.dual_read.error.count
@PrestigePvP PrestigePvP requested a review from akhilmhdh June 18, 2026 22:05
…e/on)

Run cloud entitlement reads against License Server v2 alongside v1, gated by
LICENSE_SERVER_V2_MODE so read-compare -> on is a config flip, not a code change. (PLATFOR-449)

- Replace boolean LICENSE_SERVER_V2_ENABLED with tri-state LICENSE_SERVER_V2_MODE
  (off | read-compare | on); migrate v2 SDK + usage-metering gating onto it
- getPlan is the toggle: off serves v1; read-compare serves v1 and shadow-compares v2 via
  the real client SDK (logs discrepancies + DataDog diff/error counters); on serves a v2
  entitlement set projected into the TFeatureSet shape, so getPlan callers are unchanged
- Data-driven v1<->v2 feature mapping over the full TFeatureSet + pure comparator
  (match/mismatch/v2_missing/v1_absent/indeterminate); fire-and-forget compare, no queue
- Register license.feature / license.dual_read.kind in the InfisicalCore metric allowlist
… removed boolean

Reconcile the billing-v2 surface (merged from main) with the new LICENSE_SERVER_V2_MODE,
since this branch removed LICENSE_SERVER_V2_ENABLED.

- license-v2-service isEnabled() now requires mode === "on" (every v2 billing route is gated on
  it), so the portal/checkout/overview surface is live only at full v2 cutover, not in read-compare
- admin server-config licenseServerV2Enabled reflects mode === "on" so the v2 billing UI aligns
  with the backend gate
- license-v2-service envConfig Pick moved to LICENSE_SERVER_V2_MODE
@PrestigePvP PrestigePvP force-pushed the tre/platfor-449-dual-read-v1v2-discrepancy-logging branch from b92c16e to 8f46769 Compare June 18, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant