content-grade 1.0.29 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,7 +64,14 @@ function getLicenseKey() {
64
64
 
65
65
  function getLicenseTier() {
66
66
  const cfg = loadConfig();
67
- const tier = cfg.tier || 'free';
67
+ let tier = cfg.tier || 'free';
68
+ // Fallback: if config has no tier but license file exists, read tier from there
69
+ if (tier === 'free') {
70
+ try {
71
+ const lic = JSON.parse(readFileSync(LICENSE_FILE, 'utf8'));
72
+ if (lic.tier) tier = lic.tier;
73
+ } catch {}
74
+ }
68
75
  // Collapse legacy 'starter' tier to 'pro' — Starter was removed
69
76
  return tier === 'starter' ? 'pro' : tier;
70
77
  }
@@ -137,6 +144,7 @@ function showFreeTierCTA(count) {
137
144
  console.log(` ${D}Used by 1,100+ developers. $9/mo, cancel anytime.${R}`);
138
145
  blank();
139
146
  console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
147
+ console.log(` ${D} After purchase → get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
140
148
  console.log(` ${D} Have a key? ${CY}content-grade activate <key>${R}`);
141
149
  } else if (remaining === 1) {
142
150
  // 1 run left — tease Pro features
@@ -199,6 +207,18 @@ function showUsageFooter(count) {
199
207
  maybeShowEarlyAdopterCTA(count);
200
208
  }
201
209
 
210
+ // Returns { ok: boolean, count: number, limit: number } for tier-aware daily limit checks.
211
+ // Used by batch mode to stop processing when the daily limit is reached.
212
+ // Pro tier has Infinity limit and always returns ok: true.
213
+ function checkDailyLimit() {
214
+ const tier = getLicenseTier();
215
+ const limit = TIER_LIMITS[tier] ?? TIER_LIMITS.free;
216
+ if (limit === Infinity) return { ok: true, count: 0, limit: Infinity };
217
+ const usage = getUsage();
218
+ const count = usage.count || 0;
219
+ return { ok: count < limit, count, limit };
220
+ }
221
+
202
222
  // Block a run before it starts if the free tier is exhausted.
203
223
  // Returns true (blocked) and prints a visually distinct upgrade prompt.
204
224
  // Returns false if the user may proceed.
@@ -208,6 +228,9 @@ function checkFreeTierLimit() {
208
228
  const limit = TIER_LIMITS.free;
209
229
  if (usage.count < limit) return false;
210
230
 
231
+ // Track funnel event — user hit the limit and saw the upgrade prompt
232
+ recordEvent({ event: 'free_limit_hit', version: _version });
233
+
211
234
  blank();
212
235
  hr();
213
236
  console.log(` ${YL}${B}${limit}/${limit} free analyses used — limit reached.${R}`);
@@ -221,6 +244,7 @@ function checkFreeTierLimit() {
221
244
  console.log(` ${D}Used by 1,100+ developers.${R}`);
222
245
  blank();
223
246
  console.log(` ${MG}${B}→ ${CY}${UPGRADE_LINKS.free}${R}`);
247
+ console.log(` ${D} After purchase → get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
224
248
  console.log(` ${D} Already have a key? ${CY}content-grade activate <key>${R}`);
225
249
  hr();
226
250
  blank();
@@ -1013,7 +1037,7 @@ async function cmdActivate() {
1013
1037
  if (/not found/i.test(msg) || /invalid/i.test(msg)) {
1014
1038
  blank();
1015
1039
  console.log(` ${D}If you recently purchased, retrieve your key:${R}`);
1016
- console.log(` ${CY} https://content-grade.onrender.com/api/stripe/license-key?email=YOUR_EMAIL${R}`);
1040
+ console.log(` ${CY} https://content-grade.onrender.com/my-license${R}`);
1017
1041
  }
1018
1042
  blank();
1019
1043
  process.exit(1);
@@ -2084,11 +2108,14 @@ const _showTelemNotice = _telem.isNew;
2084
2108
  contentgrade_team: 'team',
2085
2109
  };
2086
2110
  updated.tier = productKeyToTier[data.productKey] || 'pro';
2087
- } else {
2111
+ } else if (data.reason === 'revoked' || data.reason === 'subscription_expired') {
2112
+ // Only delete the key on confirmed revocation or expired subscription — NOT on 'not_found'
2113
+ // (which can happen after a Render cold start wipes the ephemeral /tmp DB).
2088
2114
  delete updated.licenseKey;
2089
2115
  delete updated.tier;
2090
2116
  try { unlinkSync(LICENSE_FILE); } catch {}
2091
2117
  }
2118
+ // If reason is 'not_found' or 'format_invalid', keep the stored key — server DB may have been wiped.
2092
2119
  saveConfig(updated);
2093
2120
  } catch {
2094
2121
  // Server unreachable — skip silently
package/bin/telemetry.js CHANGED
@@ -124,7 +124,7 @@ export function recordEvent(data) {
124
124
  const event = {
125
125
  ...data,
126
126
  package: 'content-grade',
127
- installId,
127
+ install_id: installId,
128
128
  run_count_bucket: data.run_count_bucket ?? getRunCountBucket(totalRuns),
129
129
  timestamp: new Date().toISOString(),
130
130
  };
@@ -42,16 +42,19 @@ export function registerLicenseRoutes(app) {
42
42
  // Key not in DB — could be Render cold start DB wipe. Guide user to self-serve recovery.
43
43
  const isValidFormat = /^CG-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}$/i.test(key);
44
44
  if (isValidFormat) {
45
- return { valid: false, message: 'Key not found. If you are a Pro subscriber, retrieve a fresh key at: https://content-grade.onrender.com/api/stripe/license-key?email=YOUR_EMAIL' };
45
+ return { valid: false, reason: 'not_found', message: 'Key not found. If you are a Pro subscriber, retrieve your key at: https://content-grade.onrender.com/my-license' };
46
46
  }
47
- return { valid: false, message: 'Invalid license key format.' };
47
+ return { valid: false, reason: 'format_invalid', message: 'Invalid license key format.' };
48
+ }
49
+ if (result.status !== 'active') {
50
+ return { valid: false, reason: 'revoked', message: 'License key has been revoked.' };
48
51
  }
49
52
  // Also verify Stripe subscription is still active for subscription-based keys.
50
53
  // Uses live Stripe check (with DB fallback) so DB wipes on Render cold start don't invalidate valid subscribers.
51
54
  if (result.productKey === 'contentgrade_pro' && result.email) {
52
55
  const subActive = await hasActiveSubscriptionLive(result.email);
53
56
  if (!subActive) {
54
- return { valid: false, message: 'Subscription is no longer active. Please renew at content-grade.onrender.com.' };
57
+ return { valid: false, reason: 'subscription_expired', message: 'Subscription is no longer active. Please renew at content-grade.onrender.com.' };
55
58
  }
56
59
  }
57
60
  return { valid: true, email: result.email, productKey: result.productKey };
@@ -110,7 +110,7 @@ export function registerStripeRoutes(app) {
110
110
  try {
111
111
  const data = event.data?.object;
112
112
  if (event.type === 'checkout.session.completed') {
113
- const email = (data.client_reference_id ?? data.customer_email ?? '').toLowerCase();
113
+ const email = (data.client_reference_id ?? data.customer_email ?? data.customer_details?.email ?? '').toLowerCase();
114
114
  const stripeCustomerId = data.customer;
115
115
  if (!email) {
116
116
  console.error('[stripe_webhook] checkout.session.completed missing customer email — session:', data.id, '— license key NOT generated');
@@ -250,7 +250,7 @@ export function registerStripeRoutes(app) {
250
250
  reply.type('text/html').status(402);
251
251
  return successHtml('Payment pending', '<p>Your payment has not completed yet. Please wait a moment and refresh.</p>');
252
252
  }
253
- const email = (session.client_reference_id ?? session.customer_email ?? '').toLowerCase();
253
+ const email = (session.client_reference_id ?? session.customer_email ?? session.customer_details?.email ?? '').toLowerCase();
254
254
  if (!email) {
255
255
  reply.type('text/html').status(400);
256
256
  return successHtml('Email not found', '<p>We couldn\'t identify your account. Please contact support with your Stripe receipt.</p>');
@@ -265,8 +265,7 @@ export function registerStripeRoutes(app) {
265
265
  <p>Activate it in your terminal:</p>
266
266
  <pre style="background:#111;color:#ccc;padding:12px;border-radius:6px">content-grade activate ${licenseKey}</pre>
267
267
  <p style="font-size:0.9em;color:#888">
268
- Save this key — it won't be shown again.<br>
269
- Retrieve it any time: <code>GET /api/stripe/license-key?email=${encodeURIComponent(email)}</code>
268
+ Retrieve it any time: <a href="/my-license?email=${encodeURIComponent(email)}">/my-license?email=${encodeURIComponent(email)}</a>
270
269
  </p>
271
270
  `);
272
271
  }
@@ -276,6 +275,72 @@ export function registerStripeRoutes(app) {
276
275
  return successHtml('Error retrieving order', '<p>We could not load your order details. Please contact support with your Stripe receipt.</p>');
277
276
  }
278
277
  });
278
+ // Self-service license key retrieval page — for users who paid via static Stripe link
279
+ // and weren't redirected to /checkout/success, or who lost their key.
280
+ app.get('/my-license', async (req, reply) => {
281
+ const query = req.query;
282
+ const email = (query.email ?? '').trim().toLowerCase();
283
+ if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
284
+ // Email provided — look up their key
285
+ const isActive = await hasActiveSubscriptionLive(email);
286
+ if (!isActive) {
287
+ reply.type('text/html');
288
+ return successHtml('No active subscription found', `
289
+ <p>No active Content-Grade Pro subscription was found for <strong>${email}</strong>.</p>
290
+ <p>If you just paid, it can take up to a minute for the webhook to process. Try again shortly.</p>
291
+ <p>If you believe this is an error, contact support with your Stripe receipt.</p>
292
+ <p><a href="/my-license">Try a different email</a></p>
293
+ `);
294
+ }
295
+ const customerId = getCustomerStripeId(email);
296
+ const licenseKey = upsertLicenseKey(email, customerId ?? undefined, 'contentgrade_pro');
297
+ reply.type('text/html');
298
+ return successHtml('Your Content-Grade Pro License', `
299
+ <p>Active Pro subscription confirmed for <strong>${email}</strong>.</p>
300
+ <p>Your license key:</p>
301
+ <pre style="background:#111;color:#7fff7f;padding:16px;border-radius:6px;font-size:1.1em;letter-spacing:0.05em">${licenseKey}</pre>
302
+ <p>Activate it in your terminal:</p>
303
+ <pre style="background:#111;color:#ccc;padding:12px;border-radius:6px">content-grade activate ${licenseKey}</pre>
304
+ <p style="font-size:0.9em;color:#888">
305
+ Bookmark this page to retrieve your key any time.<br>
306
+ Replace the email in the URL: <code>/my-license?email=${encodeURIComponent(email)}</code>
307
+ </p>
308
+ `);
309
+ }
310
+ // No email — show the form
311
+ reply.type('text/html');
312
+ return `<!DOCTYPE html>
313
+ <html lang="en">
314
+ <head>
315
+ <meta charset="UTF-8"/>
316
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
317
+ <title>Retrieve License Key — Content-Grade</title>
318
+ <style>
319
+ body{font-family:system-ui,sans-serif;background:#0d0d0d;color:#e8e8e8;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
320
+ .card{max-width:480px;width:90%;background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:40px}
321
+ h1{margin-top:0;font-size:1.5em}
322
+ input{width:100%;box-sizing:border-box;padding:10px 14px;background:#111;border:1px solid #444;border-radius:6px;color:#e8e8e8;font-size:1em;margin:8px 0 16px}
323
+ button{width:100%;padding:12px;background:#7fff7f;color:#000;border:none;border-radius:6px;font-size:1em;font-weight:600;cursor:pointer}
324
+ button:hover{background:#5fdf5f}
325
+ p{color:#aaa;font-size:0.9em}
326
+ a{color:#7fc4ff}
327
+ </style>
328
+ </head>
329
+ <body>
330
+ <div class="card">
331
+ <h1>Retrieve Your License Key</h1>
332
+ <p>Enter the email address you used when purchasing Content-Grade Pro.</p>
333
+ <form method="GET" action="/my-license">
334
+ <input type="email" name="email" placeholder="you@example.com" required autofocus/>
335
+ <button type="submit">Get My License Key</button>
336
+ </form>
337
+ <p style="margin-top:24px">
338
+ Don't have Pro yet? <a href="/upgrade">Upgrade for $9/mo →</a>
339
+ </p>
340
+ </div>
341
+ </body>
342
+ </html>`;
343
+ });
279
344
  // Upgrade redirect — CLI rate-limit messages point here for a clean, stable URL
280
345
  app.get('/upgrade', async (_req, reply) => {
281
346
  const proLink = process.env.STRIPE_PAYMENT_LINK_CONTENTGRADE_PRO
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "AI-powered content analysis CLI. Score any blog post, landing page, or ad copy in under 30 seconds — runs on Claude CLI, no API key needed.",
5
5
  "type": "module",
6
6
  "bin": {