content-grade 1.0.29 → 1.0.30
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.
package/bin/content-grade.js
CHANGED
|
@@ -64,7 +64,14 @@ function getLicenseKey() {
|
|
|
64
64
|
|
|
65
65
|
function getLicenseTier() {
|
|
66
66
|
const cfg = loadConfig();
|
|
67
|
-
|
|
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
|
}
|
|
@@ -199,6 +206,18 @@ function showUsageFooter(count) {
|
|
|
199
206
|
maybeShowEarlyAdopterCTA(count);
|
|
200
207
|
}
|
|
201
208
|
|
|
209
|
+
// Returns { ok: boolean, count: number, limit: number } for tier-aware daily limit checks.
|
|
210
|
+
// Used by batch mode to stop processing when the daily limit is reached.
|
|
211
|
+
// Pro tier has Infinity limit and always returns ok: true.
|
|
212
|
+
function checkDailyLimit() {
|
|
213
|
+
const tier = getLicenseTier();
|
|
214
|
+
const limit = TIER_LIMITS[tier] ?? TIER_LIMITS.free;
|
|
215
|
+
if (limit === Infinity) return { ok: true, count: 0, limit: Infinity };
|
|
216
|
+
const usage = getUsage();
|
|
217
|
+
const count = usage.count || 0;
|
|
218
|
+
return { ok: count < limit, count, limit };
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
// Block a run before it starts if the free tier is exhausted.
|
|
203
222
|
// Returns true (blocked) and prints a visually distinct upgrade prompt.
|
|
204
223
|
// Returns false if the user may proceed.
|
|
@@ -208,6 +227,9 @@ function checkFreeTierLimit() {
|
|
|
208
227
|
const limit = TIER_LIMITS.free;
|
|
209
228
|
if (usage.count < limit) return false;
|
|
210
229
|
|
|
230
|
+
// Track funnel event — user hit the limit and saw the upgrade prompt
|
|
231
|
+
recordEvent({ event: 'free_limit_hit', version: _version });
|
|
232
|
+
|
|
211
233
|
blank();
|
|
212
234
|
hr();
|
|
213
235
|
console.log(` ${YL}${B}${limit}/${limit} free analyses used — limit reached.${R}`);
|
|
@@ -1013,7 +1035,7 @@ async function cmdActivate() {
|
|
|
1013
1035
|
if (/not found/i.test(msg) || /invalid/i.test(msg)) {
|
|
1014
1036
|
blank();
|
|
1015
1037
|
console.log(` ${D}If you recently purchased, retrieve your key:${R}`);
|
|
1016
|
-
console.log(` ${CY} https://content-grade.onrender.com/
|
|
1038
|
+
console.log(` ${CY} https://content-grade.onrender.com/my-license${R}`);
|
|
1017
1039
|
}
|
|
1018
1040
|
blank();
|
|
1019
1041
|
process.exit(1);
|
|
@@ -2084,11 +2106,14 @@ const _showTelemNotice = _telem.isNew;
|
|
|
2084
2106
|
contentgrade_team: 'team',
|
|
2085
2107
|
};
|
|
2086
2108
|
updated.tier = productKeyToTier[data.productKey] || 'pro';
|
|
2087
|
-
} else {
|
|
2109
|
+
} else if (data.reason === 'revoked' || data.reason === 'subscription_expired') {
|
|
2110
|
+
// Only delete the key on confirmed revocation or expired subscription — NOT on 'not_found'
|
|
2111
|
+
// (which can happen after a Render cold start wipes the ephemeral /tmp DB).
|
|
2088
2112
|
delete updated.licenseKey;
|
|
2089
2113
|
delete updated.tier;
|
|
2090
2114
|
try { unlinkSync(LICENSE_FILE); } catch {}
|
|
2091
2115
|
}
|
|
2116
|
+
// If reason is 'not_found' or 'format_invalid', keep the stored key — server DB may have been wiped.
|
|
2092
2117
|
saveConfig(updated);
|
|
2093
2118
|
} catch {
|
|
2094
2119
|
// 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
|
|
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
|
-
|
|
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