Install
$ agentstack add skill-jcreatesgh-claude-skills-paid-extension-stripe-licensing ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
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 . 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.
Writes secrets/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
(kind mst = 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-. 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
/success paths race on eventually-consistent KV and can both mint. Derive the payload id from an HMAC of session.id (and pin ts to session.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 only
checkout.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 emailed flag 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.com link needs sk_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: 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:".
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: JCreatesGH
- Source: JCreatesGH/claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.