Paid extension stripe licensing
Skill JCreatesGH/claude-skills/paid-extension-stripe-licensing
Monetize a client-side app (Chrome/Firefox extension, desktop app, or any downloadable code) with a free tier and a paid license, using Stripe for payment and OFFLINE-verified signed license keys. The payment secret never ships in the client — a tiny Cloudflare Worker holds the Stripe secret, fulfills purchases (mints + emails a signed key), and enforces one-device activation; the client verifies keys offline against a bundled public key. Use when asked to "make this extension/app paid", add a license/upgrade flow, gate features behind a purchase, or set up Stripe fulfillment for downloadable software. Includes the key-signing CLI, a ready Cloudflare Worker, a client verifier, and the full wrangler deployment runbook.From its SKILL.md
npx -y skills add JCreatesGH/claude-skills --skill paid-extension-stripe-licensingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
3 things to look at
- reads credentialsReads from 1 credential source: `secrets/license-signing-key.json`.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 3 commands, including `node scripts/license-tools.mjs keygen` and 2 more.
SKILL.md
5.8 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
Paid client-side app with Stripe + offline license keys
The one principle that drives the whole design
Never ship a payment secret in downloadable code. Anyone can unzip an extension or decompile an app and read every file. So:
- The Stripe secret key lives ONLY in a server you control (here: a Cloudflare Worker). The client never contains it and never calls Stripe.
- License keys are signatures, verified offline. A key is an ECDSA P-256 signature over a tiny JSON payload. The client bundles only the public key and verifies keys with WebCrypto — no network needed to validate. Only the holder of the private key (you + your Worker) can mint keys, so keys cannot be forged.
- One-device enforcement is the only thing that needs the network, and it fails OPEN: if the activation server is unreachable, a validly-signed key still activates so paying customers are never locked out.
Honest framing for the user: client-side licensing deters casual sharing, not a determined tamperer. The signature stops forgery; the activation server stops broad key-sharing. This is the right trade-off for a low-price product.
Pieces (all in this skill)
| File | Role |
|---|---|
scripts/license-tools.mjs | CLI: keygen (make signing keypair), issue [--master] (mint a key), verify <key>. Run with Node. |
templates/license-client.ts | Client-side verifier + activate/deactivate + free-tier gating constants. Adapt paths/IDs to the target app. |
templates/worker.js | Cloudflare Worker: /success (post-checkout page), /webhook (backup fulfillment), /activate + /deactivate (device binding). Holds the Stripe secret. |
references/deployment-runbook.md | Step-by-step wrangler deploy + Stripe wiring, with the gotchas. |
Build order
- Generate the signing keypair:
node scripts/license-tools.mjs keygen. Writessecrets/license-signing-key.json(PRIVATE — gitignore, back up; if lost you can never mint keys again) and the public key into the client. The keygen REFUSES to overwrite an existing private key or a live public key, so a fresh clone can't accidentally invalidate every sold key. - Wire the client (
license-client.ts):getLicenseStatus()(re-verify the signature on every read so a hand-edited storage entry can't unlock the paid tier), gate features by an effective cap computed at use time — never mutate the user's stored settings to enforce a limit (it leaks into presets/saved state and silently caps a customer who pays later). Free-tier limits (watermark, page/usage cap) are applied at the point of output, checked against live license status, failing closed (treat a status-check error as unlicensed). - Generate your own master key:
node scripts/license-tools.mjs issue --master(kindmst= unlimited devices). Keep it for yourself. - Deploy the Worker (see
references/deployment-runbook.md). - Set
ACTIVATION_BASE_URLin the client to the Worker URL, rebuild, repackage. Empty = keys validate offline with no device cap.
Key payload & format
RCPDF-<base64url(payloadJSON)>.<base64url(signature)> where payload is
{v, k:'std'|'mst', id, ts, n?}. Sign with dsaEncoding: 'ieee-p1363' in
Node so the signature matches WebCrypto's ECDSA format in the client. Rename
the RCPDF prefix and storage keys per product.
Correctness gotchas (each one was a real bug)
- Mint deterministically per Checkout Session. The
/webhookand/successpaths race on eventually-consistent KV and can both mint. Derive the payloadidfrom an HMAC ofsession.id(and pintstosession.created) so any number of concurrent mints produce the IDENTICAL key → one purchase = one activation slot. - Verify the Stripe webhook with ALL
v1signatures, not just the last — during webhook-secret rotation Stripe sends several; accept if any matches. - Handle
checkout.session.async_payment_succeeded, not onlycheckout.session.completed, or delayed payment methods never fulfill. - Normalize whitespace in pasted keys (
raw.replace(/\s+/g,'')) — emails hard-wrap ~200-char keys and break naive paste. - Don't promise an email you didn't send on the success page — thread the
actual
emailedflag into the copy. - Re-verify the signature on every license read in the client, not just at activation.
- Both the Stripe dashboard AND the Chrome Web Store dashboard block browser automation — guide the user click-by-click; supply copy-paste values.
- The Stripe SECRET KEY must match the payment-link mode (a live
buy.stripe.comlink needssk_live_…). Rotate any secret ever pasted into a chat/doc.
Activation enforcement note
/activate and /deactivate use KV get-then-put, which is eventually
consistent — two near-simultaneous activations can both succeed. Fine for a
cheap product; for strict enforcement move the act:<id> state to a Durable
Object (strongly consistent CAS). Support recovery: verify the key to get its
payload id, then wrangler kv key delete --binding LICENSES "act:<id>".
What ships with it: 5 files
34.2 KB alongside SKILL.md, 3 of them executable
references/
- deployment-runbook.md3.4 KB
scripts/
- license-tools.mjsruns5.4 KB
templates/
- license-client.tsruns7.8 KB
- worker.jsruns14.9 KB
- README.md2.7 KB