content-grade 1.0.28 → 1.0.29
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
|
@@ -91,7 +91,7 @@ function incrementUsage() {
|
|
|
91
91
|
|
|
92
92
|
// Upgrade links — Free → Pro → Business → Team
|
|
93
93
|
const UPGRADE_LINKS = {
|
|
94
|
-
free: 'https://
|
|
94
|
+
free: 'https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a', // Pro $9/mo — direct checkout
|
|
95
95
|
pro: 'https://buy.stripe.com/bJefZjafO2Tz36Z2W48k80b', // Business $29/mo
|
|
96
96
|
business: 'https://buy.stripe.com/cNiaEZfA8cu9bDv4088k80c', // Team $79/mo
|
|
97
97
|
};
|
|
@@ -145,7 +145,7 @@ function showFreeTierCTA(count) {
|
|
|
145
145
|
console.log(` ${MG}→ Pro: unlimited analyses + CI mode + bulk grading. $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
146
146
|
} else {
|
|
147
147
|
// Runs remaining — light nudge with value hook
|
|
148
|
-
console.log(` ${
|
|
148
|
+
console.log(` ${WH}${count}/${limit} free analyses · ${remaining} left · Unlimited with Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
149
149
|
}
|
|
150
150
|
hr();
|
|
151
151
|
}
|
|
@@ -194,7 +194,7 @@ function showUsageFooter(count) {
|
|
|
194
194
|
} else if (remaining === 1) {
|
|
195
195
|
console.log(` ${YL}[ ${count}/${limit} free runs used · 1 left. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
|
|
196
196
|
} else {
|
|
197
|
-
console.log(` ${D}[ ${count}/${limit} free runs
|
|
197
|
+
console.log(` ${D}[ ${count}/${limit} free runs used · ${remaining} left. Unlimited: ${UPGRADE_LINKS.free} ]${R}`);
|
|
198
198
|
}
|
|
199
199
|
maybeShowEarlyAdopterCTA(count);
|
|
200
200
|
}
|
|
@@ -210,7 +210,7 @@ function checkFreeTierLimit() {
|
|
|
210
210
|
|
|
211
211
|
blank();
|
|
212
212
|
hr();
|
|
213
|
-
console.log(` ${YL}${B}${limit}/${limit} free analyses used
|
|
213
|
+
console.log(` ${YL}${B}${limit}/${limit} free analyses used — limit reached.${R}`);
|
|
214
214
|
blank();
|
|
215
215
|
console.log(` ${D}Pro removes the cap — unlimited analyses, $9/mo:${R}`);
|
|
216
216
|
console.log(` ${D}├── Unlimited analyses — no cap, ever${R}`);
|
|
@@ -1008,7 +1008,13 @@ async function cmdActivate() {
|
|
|
1008
1008
|
} else {
|
|
1009
1009
|
process.stdout.write('\n');
|
|
1010
1010
|
blank();
|
|
1011
|
-
|
|
1011
|
+
const msg = data.message || `Invalid or expired license key.`;
|
|
1012
|
+
fail(msg);
|
|
1013
|
+
if (/not found/i.test(msg) || /invalid/i.test(msg)) {
|
|
1014
|
+
blank();
|
|
1015
|
+
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}`);
|
|
1017
|
+
}
|
|
1012
1018
|
blank();
|
|
1013
1019
|
process.exit(1);
|
|
1014
1020
|
}
|
|
@@ -2047,6 +2053,48 @@ const _telem = initTelemetry();
|
|
|
2047
2053
|
// Defer first-run telemetry notice so users see value first
|
|
2048
2054
|
const _showTelemNotice = _telem.isNew;
|
|
2049
2055
|
|
|
2056
|
+
// ── License key startup validation ───────────────────────────────────────────
|
|
2057
|
+
// Background check: re-validate stored license key against server every 7 days.
|
|
2058
|
+
// Non-blocking — never delays the CLI. Clears invalid/revoked keys silently.
|
|
2059
|
+
(async () => {
|
|
2060
|
+
const storedKey = getLicenseKey();
|
|
2061
|
+
if (!storedKey) return;
|
|
2062
|
+
const cfg = loadConfig();
|
|
2063
|
+
const lastCheck = cfg?.lastLicenseCheck;
|
|
2064
|
+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
|
|
2065
|
+
if (lastCheck && Date.now() - new Date(lastCheck).getTime() < sevenDays) return;
|
|
2066
|
+
const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.onrender.com';
|
|
2067
|
+
try {
|
|
2068
|
+
const controller = new AbortController();
|
|
2069
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
2070
|
+
const response = await fetch(`${serverUrl}/api/license/validate`, {
|
|
2071
|
+
method: 'POST',
|
|
2072
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2073
|
+
body: JSON.stringify({ key: storedKey }),
|
|
2074
|
+
signal: controller.signal,
|
|
2075
|
+
});
|
|
2076
|
+
clearTimeout(timer);
|
|
2077
|
+
const data = await response.json();
|
|
2078
|
+
const updated = loadConfig();
|
|
2079
|
+
updated.lastLicenseCheck = new Date().toISOString();
|
|
2080
|
+
if (data.valid) {
|
|
2081
|
+
const productKeyToTier = {
|
|
2082
|
+
contentgrade_starter: 'starter',
|
|
2083
|
+
contentgrade_pro: 'pro',
|
|
2084
|
+
contentgrade_team: 'team',
|
|
2085
|
+
};
|
|
2086
|
+
updated.tier = productKeyToTier[data.productKey] || 'pro';
|
|
2087
|
+
} else {
|
|
2088
|
+
delete updated.licenseKey;
|
|
2089
|
+
delete updated.tier;
|
|
2090
|
+
try { unlinkSync(LICENSE_FILE); } catch {}
|
|
2091
|
+
}
|
|
2092
|
+
saveConfig(updated);
|
|
2093
|
+
} catch {
|
|
2094
|
+
// Server unreachable — skip silently
|
|
2095
|
+
}
|
|
2096
|
+
})();
|
|
2097
|
+
|
|
2050
2098
|
// Smart path detection: first arg is a path if it starts with ., /, ~ or is an existing FS entry
|
|
2051
2099
|
function looksLikePath(s) {
|
|
2052
2100
|
if (!s) return false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateLicenseKey, getLicenseKeysForEmail } from '../services/license.js';
|
|
2
|
-
import {
|
|
2
|
+
import { hasActiveSubscriptionLive } from '../services/stripe.js';
|
|
3
3
|
// Simple in-memory rate limiter: max 10 calls per IP per minute
|
|
4
4
|
const _validateRateLimiter = new Map();
|
|
5
5
|
function checkRateLimit(ip) {
|
|
@@ -39,11 +39,17 @@ export function registerLicenseRoutes(app) {
|
|
|
39
39
|
}
|
|
40
40
|
const result = validateLicenseKey(key);
|
|
41
41
|
if (!result.valid) {
|
|
42
|
-
|
|
42
|
+
// Key not in DB — could be Render cold start DB wipe. Guide user to self-serve recovery.
|
|
43
|
+
const isValidFormat = /^CG-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}$/i.test(key);
|
|
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' };
|
|
46
|
+
}
|
|
47
|
+
return { valid: false, message: 'Invalid license key format.' };
|
|
43
48
|
}
|
|
44
|
-
// Also verify Stripe subscription is still active for subscription-based keys
|
|
49
|
+
// Also verify Stripe subscription is still active for subscription-based keys.
|
|
50
|
+
// Uses live Stripe check (with DB fallback) so DB wipes on Render cold start don't invalidate valid subscribers.
|
|
45
51
|
if (result.productKey === 'contentgrade_pro' && result.email) {
|
|
46
|
-
const subActive =
|
|
52
|
+
const subActive = await hasActiveSubscriptionLive(result.email);
|
|
47
53
|
if (!subActive) {
|
|
48
54
|
return { valid: false, message: 'Subscription is no longer active. Please renew at content-grade.onrender.com.' };
|
|
49
55
|
}
|
package/package.json
CHANGED