Skip to content

fix(licenses): show real activation count (token-independent), fail loud on machine mgmt#271

Merged
kilbot merged 4 commits into
mainfrom
fix/keygen-activation-count
Jul 6, 2026
Merged

fix(licenses): show real activation count (token-independent), fail loud on machine mgmt#271
kilbot merged 4 commits into
mainfrom
fix/keygen-activation-count

Conversation

@kilbot

@kilbot kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Fix "0 of N activations" — properly (token-independent by design)

Every customer's Licenses page showed "0 of N activations" even when machines were active. This is the code half of the fix; the infra half (setting KEYGEN_API_TOKEN in prod) was handled separately.

Root cause

Activation count came from the authed Keygen machine-list call (getLicenseMachines), which needs KEYGEN_API_TOKEN. When the token was absent (as it was in prod) or the call failed, resolveLicenseReference silently fell back to a path that hardcoded machines: [] → a misleading 0. The same bug existed in the plugin-facing validateLicense.

The proper fix — don't depend on an admin token for a read customers need

Keygen's public validate-key response already carries the authoritative activation count at relationships.machines.meta.count — no credential required. So:

  • LicenseDetail gains activationCount, mapped from that field. The UI renders "X of Y" from it, never machines.length.
  • resolveLicenseReference is now validate-key-primary: correct status + count with zero credentials. The machine detail list (fingerprints, for management/deactivation) is optional enrichment, attempted only when a token is configured. If enrichment is unavailable or fails, the list is empty but the count stays correct — honest "N activations (details unavailable)", never a wrong 0.
  • The plugin-facing validateLicense count now comes from validate-key too.
  • Fail loud: authHeaders() throws KeygenAuthNotConfiguredError when the token is missing (instead of silently sending Bearer undefined → 401 → "0"). The machine-deactivation route surfaces it as a clear 503 "Machine management is not configured" rather than a vague 500.

Net effect: activation counts are correct with or without the admin token; the token only gates machine management (list + deactivate), which now fails loudly when misconfigured.

Verified against live Keygen

POST /v1/licenses/actions/validate-key   (no auth header)
→ meta.valid: true
→ relationships.machines.meta.count: 1   ← the authoritative count

for a license the customer would see as "1 of 2".

Validation

  • ✅ 1211 unit tests pass (vitest run), incl. new tests: correct count with no token; enrichment when token present; count preserved when enrichment fails; KeygenAuthNotConfiguredError → 503.
  • next build clean, ESLint clean.
  • ⏳ End-to-end confirmation is post-deploy: a customer's Licenses page shows the real "X of Y".

Independent of #268 (view-as Orders fields) — touches different files.

Summary by CodeRabbit

  • New Features

    • License details now display an authoritative activation count from Keygen, even when machine-management access isn’t available.
    • Machine list enrichment is applied only when authenticated access is possible.
  • Bug Fixes

    • License resolution is more resilient: activation counts are preserved even if machine lookup fails.
    • Deleting a machine now returns a clearer 503 “Machine management is not configured” response when machine management isn’t set up.
  • Tests

    • Expanded unit/integration and UI test coverage for activation counts, conditional enrichment, and the new 503 behavior.

…an admin-token call

The account Licenses page showed '0 of N activations' for every customer.
Root cause: the count came from the authed Keygen machine-list call
(getLicenseMachines), which requires KEYGEN_API_TOKEN. Whenever that token was
absent or the call failed, resolveLicenseReference silently fell back to a path
that hardcoded machines:[] -> a misleading 0.

Proper fix (token-independent by design):
- Keygen's PUBLIC validate-key response carries the authoritative activation
  count at relationships.machines.meta.count. Map it into LicenseDetail as
  activationCount and render 'X of Y' from that, never machines.length.
- resolveLicenseReference is now validate-key-primary: correct status + count
  with no credential. The machine DETAIL list is optional enrichment, attempted
  only when a token is configured (canManageMachines()); failure leaves the list
  empty but the count intact -- an honest 'N (details unavailable)', never a
  wrong 0.
- The plugin-facing validateLicense() count now comes from validate-key too.
- Fail LOUD: authHeaders() throws KeygenAuthNotConfiguredError when the token is
  missing; the machine-deactivation route surfaces it as a clear 503 instead of
  a vague 500.

Verified against live Keygen: validate-key returns machines.meta.count for a
known 1-activation license.

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

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 124c10b2-2393-4b53-98a9-8fda010874eb

📥 Commits

Reviewing files that changed from the base of the PR and between acfed43 and 6084e71.

📒 Files selected for processing (6)
  • playwright.config.ts
  • src/lib/account-order-projection.test.ts
  • src/lib/customer-licenses.test.ts
  • src/lib/customer-licenses.ts
  • src/lib/discord/connected-member-service.test.ts
  • src/lib/discord/default-sync.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/lib/discord/default-sync.test.ts
  • src/lib/discord/connected-member-service.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/customer-licenses.ts

📝 Walkthrough

Walkthrough

This PR adds Keygen auth gating, derives activation counts from validate-key metadata, conditionally enriches customer licenses with machine lists, and updates the machine deactivation route and UI to use the new license data.

Changes

Activation count and Keygen auth guard

Layer / File(s) Summary
License client auth guard and activation count mapping
src/services/core/external/license-client.ts, src/services/core/external/license-client.test.ts, src/types/license.ts, playwright.config.ts
Adds KeygenAuthNotConfiguredError, canAuthenticate()/canManageMachines, and maps activationCount from machines.meta.count; validateLicense uses this as authoritative and makes machine-list fetch best-effort/conditional; tests, docs, and E2E config are updated accordingly.
Customer license resolution with optional machine enrichment
src/lib/customer-licenses.ts, src/lib/customer-licenses.test.ts
Reworks resolveLicenseReference to validate via validateLicenseKey first, preserves authoritative activationCount, adds enrichWithMachineList to conditionally attach machine data, and refines null-return logic; tests cover no-token, token-present, key-only, and enrichment-failure paths.
Machine route error handling and activation display
src/app/api/account/licenses/[licenseId]/machines/[machineId]/route.ts, src/app/api/account/licenses/[licenseId]/machines/[machineId]/route.test.ts, src/components/account/licenses-client.tsx, src/components/account/licenses-client.test.tsx, src/lib/account-order-projection.test.ts, src/lib/discord/connected-member-service.test.ts, src/lib/discord/default-sync.test.ts
Route returns 503 with "Machine management is not configured" on KeygenAuthNotConfiguredError; the UI displays license.activationCount instead of machines.length; test factories and fixtures are updated with activationCount.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant licenseClient
  participant KeygenAPI

  Client->>licenseClient: validateLicense(key)
  licenseClient->>KeygenAPI: validateLicenseKey(key)
  KeygenAPI-->>licenseClient: license + machines.meta.count
  licenseClient->>licenseClient: canAuthenticate()?
  alt token present
    licenseClient->>KeygenAPI: fetch machine list
    KeygenAPI-->>licenseClient: machines (best-effort)
  else no token
    licenseClient-->>licenseClient: skip machine list, use activationCount
  end
  licenseClient-->>Client: activationsCount from license.activationCount
Loading
sequenceDiagram
  participant Caller
  participant resolveLicenseReference
  participant licenseClient
  participant enrichWithMachineList

  Caller->>resolveLicenseReference: resolve(licenseRef)
  resolveLicenseReference->>licenseClient: validateLicenseKey(key)
  licenseClient-->>resolveLicenseReference: base license + activationCount
  resolveLicenseReference->>enrichWithMachineList: enrich(base, id)
  enrichWithMachineList->>licenseClient: canManageMachines()?
  alt token configured
    enrichWithMachineList->>licenseClient: getLicenseMachines(id)
    licenseClient-->>enrichWithMachineList: machines list
    enrichWithMachineList-->>resolveLicenseReference: base with machines
  else no token or failure
    enrichWithMachineList-->>resolveLicenseReference: unmodified base license
  end
  resolveLicenseReference-->>Caller: resolved license
Loading

Possibly related PRs

  • wcpos/wcpos-com#136: Both PRs modify validateLicense in src/services/core/external/license-client.ts, changing how Keygen status/auth info flows into the returned payload.
  • wcpos/wcpos-com#184: Both PRs modify src/lib/customer-licenses.ts resolution flow, including getResolvedCustomerLicenses and license enrichment behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: token-independent activation counts and explicit failure when machine management is unavailable.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/keygen-activation-count

Comment @coderabbitai help to get the list of available commands.

Use validate-key's canonical license.id (base.id) instead of reference.id so
key-only and re-issued (stale-id) references still fetch their machine detail
list when a token is configured. Count was already correct; this fixes the
detail-list/deactivation UI for those references.
@kilbot

kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Live verification (review item #1 — the load-bearing assumption): confirmed against prod Keygen that the public validate-key response includes the activation count:

POST https://license.wcpos.com/v1/licenses/actions/validate-key   (no auth header)
→ meta.valid: true, code: VALID
→ relationships.machines.meta.count: 1

for license 3fdb… (key 10C2A5-…), which a customer would see as "1 of 2". So the no-token count path is not a no-op — it returns the real count. Also applied review item #2 (enrich via the resolved canonical license.id, commit d71584f).

Deferred (pre-existing, not a regression): the Discord member-removal route (.../discord/members/[memberId]) calls authed Keygen ops without a typed guard, so it 500s (not 503) when the token is absent — same as before this change. Worth a follow-up.

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

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@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: acfed4374c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/customer-licenses.ts Outdated
Comment thread src/lib/customer-licenses.ts Outdated

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/components/account/licenses-client.tsx (1)

445-445: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Machine list section can vanish even with a positive activation count.

license.machines.length > 0 gates the "Activated machines" section, but per the new contract machines can be empty while activationCount > 0 (unauthenticated enrichment). Users would see e.g. "2 of 5 activations" with no list/management controls beneath it, with no explanation. Since this is an accepted tradeoff per the PR description, consider (optionally) surfacing a note like "machine details unavailable" when activationCount > 0 && machines.length === 0 for clarity, rather than silently omitting the section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/account/licenses-client.tsx` at line 445, The Activated
machines section in licenses-client.tsx is currently hidden whenever
`license.machines.length > 0` is false, which can make the UI disappear even
when `activationCount` is positive. Update the rendering condition around the
machine list in `licenses-client.tsx` so it also handles the unauthenticated
enrichment case where `activationCount > 0` but `machines` is empty. Use the
`license.machines`, `license.activationCount`, and the existing “Activated
machines” block to locate the change, and optionally add a small note such as
“machine details unavailable” when counts exist but no machine records are
present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/customer-licenses.test.ts`:
- Around line 37-38: The test helper wrapper for canManageMachines is causing a
TypeScript spread/signature mismatch because mockCanManageMachines is a
zero-argument vi.fn and does not accept forwarded args. Update the
canManageMachines mock in customer-licenses.test.ts to call
mockCanManageMachines directly with no parameters, matching the production
canManageMachines contract and removing the unnecessary (...args: unknown[])
wrapper.

In `@src/lib/customer-licenses.ts`:
- Around line 45-59: Use the validated license id for machine enrichment:
enrichWithMachineList currently receives reference.id, which can be missing even
after resolveLicenseReference() successfully validates a key-only license.
Update the caller in customer license resolution to pass base.id into
enrichWithMachineList so machine details are still fetched when only the
validated base license has an id and a Keygen token is available.

---

Nitpick comments:
In `@src/components/account/licenses-client.tsx`:
- Line 445: The Activated machines section in licenses-client.tsx is currently
hidden whenever `license.machines.length > 0` is false, which can make the UI
disappear even when `activationCount` is positive. Update the rendering
condition around the machine list in `licenses-client.tsx` so it also handles
the unauthenticated enrichment case where `activationCount > 0` but `machines`
is empty. Use the `license.machines`, `license.activationCount`, and the
existing “Activated machines” block to locate the change, and optionally add a
small note such as “machine details unavailable” when counts exist but no
machine records are present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99f2bff3-120d-4e92-a30d-6802a386d67a

📥 Commits

Reviewing files that changed from the base of the PR and between 1aa4f93 and acfed43.

📒 Files selected for processing (9)
  • src/app/api/account/licenses/[licenseId]/machines/[machineId]/route.test.ts
  • src/app/api/account/licenses/[licenseId]/machines/[machineId]/route.ts
  • src/components/account/licenses-client.test.tsx
  • src/components/account/licenses-client.tsx
  • src/lib/customer-licenses.test.ts
  • src/lib/customer-licenses.ts
  • src/services/core/external/license-client.test.ts
  • src/services/core/external/license-client.ts
  • src/types/license.ts

Comment thread src/lib/customer-licenses.test.ts Outdated
Comment thread src/lib/customer-licenses.ts Outdated
…ichment

CI type-check (tsc over test files, which next build skips) failed: three test
LicenseDetail factories lacked the now-required activationCount, and a mock
wrapper spread args into a zero-arg vi.fn. Add activationCount to the factories
and drop the spread (CodeRabbit).

Also address Codex P2: enrichWithMachineList no longer overwrites the
authoritative validate-key activationCount with machines.length — getLicenseMachines
returns a single paginated page, so a partial page could undercount. Keep the
validate-key count; use the fetched machines as the detail list only.

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

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@kilbot

kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Triage — review + CI (fix skill)

# Source File Class Category Decision Outcome
1 Codex customer-licenses.ts:54 Logic Major Fix Keep authoritative validate-key count; getLicenseMachines is a single paginated page, so overwriting with machines.length could undercount. Enrichment now sets the detail list only. 64a3ca7
2 Codex + CodeRabbit customer-licenses.ts:81 Logic Major Fix Enrich via the resolved canonical base.id, so key-only / re-issued (stale-id) refs still get the machine list. d71584f (+ new key-only test)
3 CodeRabbit customer-licenses.test.ts:38 Mechanical Critical Fix Dropped spread into the zero-arg canManageMachines mock. 64a3ca7
4 CI Test → Type check Mechanical Blocker Fix 3 other LicenseDetail test factories lacked the now-required activationCount; added it. (next build skips test files; CI runs tsc over all.) 64a3ca7
5 CI E2E Tests (license-flows, checkout journey) Logic Blocker Fix authHeaders() now fails loud without a token; the E2E mock harness set no KEYGEN_API_TOKEN (mock ignores auth, so the old id-path silently worked with Bearer undefined). Setting the token in the E2E env — matching prod, where it is now set — restores the id-path + machine-management tests. Preserves the parallel-worker suffix isolation (id-path stays authoritative for id-referenced personas; the validate-key-primary path is unit-tested). applying now

The greptile bot comments are a trial-credit notice, not findings. Replying to + resolving each thread after the E2E fix pushes.

authHeaders() now fails loud (KeygenAuthNotConfiguredError) when the token is
absent; the mock harness set none, so the license/machine flows 500'd. The mock
ignores auth, but the value must be present — matching prod, where KEYGEN_API_TOKEN
is now configured — so the id-path resolves and machine-management e2e tests run.

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

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@kilbot

kilbot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

✅ Merge-ready

All review threads replied-to + resolved (4/4), all CI green:

Check Result
Test (unit + tsc) ✅ pass
E2E Tests ✅ pass
CodeQL / Analyze ✅ pass
CodeRabbit ✅ pass

Final commits this round:

  • 64a3ca7 — type-check fixes (3 test factories + mock spread) + Codex "preserve validate-key count on enrichment"
  • d71584f — Codex/CodeRabbit "enrich via resolved canonical base.id" (+ key-only test)
  • 6084e71 — E2E: set KEYGEN_API_TOKEN in the mocked web-server env (matches prod), fixing the license-flows + checkout-journey failures

reviewDecision is empty (no required human reviewer) — ready for your merge.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-jnp08kuhi-wcpos.vercel.app

@kilbot kilbot merged commit 09e23f7 into main Jul 6, 2026
9 checks passed
@kilbot kilbot deleted the fix/keygen-activation-count branch July 6, 2026 13:59
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