Skip to content

feat: license verification library + minting service#132

Merged
blove merged 51 commits into
mainfrom
feat/license-verification
Apr 21, 2026
Merged

feat: license verification library + minting service#132
blove merged 51 commits into
mainfrom
feat/license-verification

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented Apr 20, 2026

Summary

Ships two plans together on a single branch:

  • @cacheplane/licensing — offline Ed25519 license verification, grace-window status evaluation, nag UX with per-package dedupe, non-blocking telemetry, and a runLicenseCheck orchestrator. Integrated into @cacheplane/agent, @cacheplane/chat, and @cacheplane/render at provider init.
  • apps/minting-service — Vercel serverless app that receives Stripe webhooks (checkout.session.completed, customer.subscription.updated, customer.subscription.deleted), signs Ed25519 license tokens, persists them via a new @cacheplane/db lib (Drizzle + Postgres), and emails them via Resend. Includes idempotency on processed_events, compensating delete on handler failure, material-change detection on subscription updates, and an operator remint CLI.

Plans:

  • docs/superpowers/plans/2026-04-13-license-verification.md (prior)
  • docs/superpowers/plans/2026-04-20-minting-service.md (this session — 26 tasks, all complete)

Test Plan

  • npx nx run minting-service:test — 42 tests passing (6 files)
  • npx nx run minting-service:lint — 0 errors (24 expected any warnings)
  • npx tsc --noEmit -p apps/minting-service/tsconfig.app.json — passes
  • npx nx run db:test — Drizzle + Testcontainers integration (requires Docker)
  • npx nx run licensing:test — full licensing lib suite
  • Deploy apps/minting-service as a Vercel preview, configure test Stripe + preview Postgres + Resend keys, run DATABASE_URL=<preview> npx nx run db:db:migrate, then:
    • curl <preview>/api/health{"ok":true}
    • stripe trigger checkout.session.completed → row lands in licenses, email arrives
    • Paste emitted token into a sandbox app with CACHEPLANE_LICENSE=<token>runLicenseCheck reports active
    • stripe trigger customer.subscription.deletedrevoked_at set
    • nx run minting-service:remint --sub=<id> --dry-run → refuses revoked license

Known out-of-scope issues

  • Angular consumers of @cacheplane/licensing (demo, cockpit-registry, ui-react, cockpit-chat-generative-ui-angular) fail to compile because the licensing lib uses Node-only `Buffer`/`process`. These predate the minting-service plan; fix is a separate browser-safe plan.
  • `apps/minting-service` has no `build` target — `tsc --noEmit` via `vercel.json` is the build check.

Follow-ups (from final review)

  • Harden remint tier cast in `scripts/remint.ts:60,71` (currently casts `license.tier` directly; should flow through `extractTier`-style validation).
  • Add a `resetStripeForTests` export on the singleton in `src/lib/stripe.ts`.
  • Short-circuit no-op upsert on unchanged claims in `handleSubscriptionUpdated`.

🤖 Generated with Claude Code

blove and others added 30 commits April 20, 2026 07:41
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The original T10 test design had a fresh-keypair/LICENSE_PUBLIC_KEY mismatch:
the silent-verify test signed with an ephemeral pair but the provider verified
against the compile-time embedded public key, making the test unprovable
without either committing a private key fixture (security smell) or stripping
the testing exclude from the published lib (ships test helpers to consumers).

Revise T10-T12 to add an @internal __licensePublicKey?: Uint8Array override on
each config interface, defaulting to LICENSE_PUBLIC_KEY in production. Tests
pass the ephemeral pair's public key via this hook, so nothing in the repo
ever needs to sign with or store a private key.

Also add explicit guardrails to each task forbidding: committing private-key
fixtures, mutating testing/keypair.ts, removing tsconfig exclusions, and
unilaterally patching test-setup.ts or tsconfig.base.json when jsdom/Nx
environment issues appear — failures should escalate to the controller.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Fires a fire-and-forget runLicenseCheck() at provideAgent() construction,
threading the user-supplied license through to offline Ed25519 verification
against the build-time embedded LICENSE_PUBLIC_KEY. Never blocks DI.

Adds an @internal __licensePublicKey?: Uint8Array hook on AgentConfig so
tests can verify against an ephemeral pair without compiling a second key
into the package — the test mints generateKeyPair() at runtime and passes
kp.publicKey through the hook. Nothing in the repo signs with or stores a
private key; the only fixture in libs/licensing/fixtures/ remains the public
key generated at prebuild from CACHEPLANE_LICENSE_PUBLIC_KEY or the
deterministic dev fallback.

Carves testing helpers into a source-only @cacheplane/licensing/testing
subpath (libs/licensing/src/testing.ts) registered via tsconfig paths and
excluded from tsconfig.lib.json so the published dist/ stays free of
sign/verify helpers. Downstream consumers cannot import
@cacheplane/licensing/testing.

Drops the baseUrl: "." override from libs/agent/tsconfig.json because it
shifted path resolution relative to the agent dir and broke @cacheplane/
licensing resolution; chat and render tsconfigs never set it either.

Adds a scoped sha512Async patch to libs/agent/src/test-setup.ts: @noble/
ed25519 calls crypto.subtle.digest() which jsdom rejects for cross-realm
TypedArrays. The patch routes sha512 through Node's crypto module in the
agent test env only — no effect on production code or the published package.

Revises the plan doc in the same commit so T11/T12 pick up the corrected
testing-subpath / sha512-patch / tsconfig pattern.

40/40 agent tests pass; 30/30 licensing tests pass; agent build succeeds
and dist/libs/licensing contains no testing helpers.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Render's tsconfig.json has the same baseUrl: "." override as agent had
pre-T10, which will break @cacheplane/licensing resolution the same way.
Call it out explicitly as Step 0 so T11 doesn't rediscover it.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Two scaffolding issues surfaced by the T13 sanity sweep blocked the
published @cacheplane/licensing package from loading at runtime:

1. libs/licensing/tsconfig.lib.json inherited emitDeclarationOnly: true
   from tsconfig.base.json, so `@nx/js:tsc` emitted only .d.ts files.
   Override with emitDeclarationOnly: false so .js is emitted.

2. Relative imports in licensing source used extensionless specifiers
   (e.g., `./lib/verify-license`). TS moduleResolution: bundler accepts
   these at typecheck time but Node ESM requires explicit `.js` at
   runtime. Add `.js` extensions to all relative imports in the lib
   build set (src/index.ts + 5 non-spec files under src/lib/).

Spec files and src/lib/testing/ are excluded from the lib build, so
their imports are untouched.

Verification:
- npx nx run-many -t test -p licensing,agent,render,chat: all pass
- npx nx run-many -t build -p licensing,agent,render,chat: all pass
- Node nag-path smoke test against dist/libs/licensing/src/index.js:
  fires `[cacheplane] @cacheplane/test: no license key detected...`
  and exits 0 (telemetry failure swallowed).
The nine-line `inferNoncommercial()` helper was duplicated verbatim in
`provideAgent`, `provideRender`, and `provideChat`. Extract it into
`@cacheplane/licensing` and re-export from the public index so all three
providers share one implementation.

Behavior is unchanged — agent/render/chat specs that cover the nag warn
path still pass.
The T13 sanity sweep ran lint across licensing/agent/render/chat and
surfaced a pile of pre-existing errors unrelated to the license work.
Clear them so the four packages are lint-clean for release.

- libs/render: rename stub selectors in views.spec.ts to the
  `render-` prefix required by @angular-eslint/component-selector.
- libs/chat/package.json: declare the missing `@angular/platform-browser`
  peer dependency (flagged by @nx/dependency-checks).
- libs/chat a2ui catalog components: associate labels with their
  form controls via a per-class id counter (text-field, choice-picker,
  slider, date-time-input) and make the modal backdrop a proper
  button role with keyboard handlers (modal.component.ts).
License minting service design: Stripe webhook → signed license token → email delivery.
Covers architecture, data model, webhook flow, email content, env vars, deployment,
manual re-mint CLI, testing strategy, and out-of-scope boundaries.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Spec used 'dev-seat' but @cacheplane/licensing's LicenseTier is 'developer-seat'.
Corrected to match the library's existing type.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
26-task plan across 9 phases: extend @cacheplane/licensing with signLicense;
scaffold @cacheplane/db lib with Drizzle schema + migrations + queries;
scaffold apps/minting-service with pure modules for env/tier/sign/email;
handlers with idempotency + compensating-delete + material-change check;
webhook endpoint; manual re-mint CLI; operator runbook. TDD throughout.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Remove test-only signLicense from testing/keypair.ts now that the
production signLicense helper exists. verify-license.spec.ts now
tests against the real signer.
The @nx/vite:test executor is deprecated and will be removed in Nx 23.
@nx/vitest:test is a drop-in replacement (same configFile option).

Affects 13 project.json files across libs, apps, and e2e.
blove and others added 20 commits April 20, 2026 13:16
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Also fixes two type-level issues exposed when the api/ directory
brought tsc --noEmit into scope:
- tsconfig.app.json: disable composite/declaration locally since
  libs are consumed via tsconfig path aliases (not project
  references), so composite mode can't see imported lib sources.
- stripe.ts: cast apiVersion '2024-06-20' through any because the
  SDK's LatestApiVersion literal only admits '2026-03-25.dahlia';
  we pin to 2024-06-20 at runtime for subscription shape stability.
Uses npm ci (not pnpm — this repo is npm-based) and tsc --noEmit
as the build command since api/*.ts files are compiled by Vercel's
own runtime. The @cacheplane/db and @cacheplane/licensing imports
resolve via tsconfig path aliases at build-time; runtime
resolution will need verification during first deploy.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment Apr 20, 2026 11:43pm

Request Review

The licensing lib was using Node-only APIs (`Buffer`, `process`) in its
base64url helpers and telemetry opt-out check, which caused Angular
compilers (demo, cockpit-*, website-embedded bundles) to fail with
`TS2591: Cannot find name 'Buffer'/'process'`.

- license-token.ts: swap `Buffer.from(b64, 'base64')` for `atob` +
  Uint8Array conversion.
- sign-license.ts: swap `Buffer.from(bytes).toString('base64')` for
  `btoa` + String.fromCharCode conversion.
- telemetry.ts: read `process.env` via `globalThis` so the bare
  `process` identifier is never referenced.
- license-token.ts + telemetry.ts: switch Record<string, unknown>
  dot-access to bracket-access to satisfy Angular's
  `noPropertyAccessFromIndexSignature`.

No behavior change: atob/btoa are available in Node 16+ and all
browsers; all 37 licensing unit tests and 42 minting-service tests
still pass; website, demo, and cockpit Angular builds now succeed.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@blove blove merged commit 9fa6136 into main Apr 21, 2026
14 checks passed
@blove blove deleted the feat/license-verification branch April 21, 2026 03:18
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