feat: license verification library + minting service#132
Merged
Conversation
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.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…compensating delete
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 arunLicenseCheckorchestrator. Integrated into@cacheplane/agent,@cacheplane/chat, and@cacheplane/renderat 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/dblib (Drizzle + Postgres), and emails them via Resend. Includes idempotency onprocessed_events, compensating delete on handler failure, material-change detection on subscription updates, and an operatorremintCLI.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 expectedanywarnings)npx tsc --noEmit -p apps/minting-service/tsconfig.app.json— passesnpx nx run db:test— Drizzle + Testcontainers integration (requires Docker)npx nx run licensing:test— full licensing lib suiteapps/minting-serviceas a Vercel preview, configure test Stripe + preview Postgres + Resend keys, runDATABASE_URL=<preview> npx nx run db:db:migrate, then:curl <preview>/api/health→{"ok":true}stripe trigger checkout.session.completed→ row lands inlicenses, email arrivesCACHEPLANE_LICENSE=<token>—runLicenseCheckreports activestripe trigger customer.subscription.deleted→revoked_atsetnx run minting-service:remint --sub=<id> --dry-run→ refuses revoked licenseKnown out-of-scope issues
@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.Follow-ups (from final review)
🤖 Generated with Claude Code