seo-intel 1.1.7 → 1.1.9
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/.env.example +3 -1
- package/CHANGELOG.md +54 -0
- package/LICENSE +4 -5
- package/README.md +3 -3
- package/cli.js +136 -16
- package/crawler/index.js +11 -2
- package/crawler/subdomain-discovery.js +1 -1
- package/lib/gate.js +5 -5
- package/lib/license.js +150 -69
- package/lib/updater.js +19 -18
- package/package.json +3 -2
- package/seo-audit.js +2 -2
- package/server.js +3 -5
- package/setup/wizard.html +352 -32
package/lib/license.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SEO Intel — License System
|
|
3
3
|
*
|
|
4
|
-
* Validation
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. No key → Free tier
|
|
4
|
+
* Validation flow:
|
|
5
|
+
* 1. SEO_INTEL_LICENSE → activate/validate against Lemon Squeezy License API
|
|
6
|
+
* 2. No key → Free tier
|
|
8
7
|
*
|
|
9
8
|
* Local cache: ~/.seo-intel/license-cache.json
|
|
10
9
|
* - LS keys: cache 24h, stale up to 7 days if API unreachable
|
|
11
|
-
* - Froggo tokens: cache 24h, stale up to 24h if API unreachable
|
|
12
10
|
* - Beyond stale limit → degrade to free tier + warn
|
|
13
11
|
*
|
|
12
|
+
* Activation flow (Lemon Squeezy):
|
|
13
|
+
* - First run: POST /v1/licenses/activate → stores instance_id locally
|
|
14
|
+
* - Subsequent runs: POST /v1/licenses/validate with instance_id
|
|
15
|
+
* - Machine-specific: instance_name = "seo-intel-<machineId>"
|
|
16
|
+
*
|
|
14
17
|
* Free tier: crawl + raw HTML export only. No AI extraction or analysis.
|
|
15
|
-
* Solo (€19.99/mo or €199
|
|
18
|
+
* Solo (€19.99/mo or €199.99/yr): Full AI extraction + analysis, all commands.
|
|
16
19
|
* Agency: Later phase — not sold yet.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
22
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
20
23
|
import { join, dirname } from 'path';
|
|
21
24
|
import { fileURLToPath } from 'url';
|
|
22
25
|
import { hostname, userInfo, platform } from 'os';
|
|
@@ -32,8 +35,6 @@ const CACHE_PATH = join(CACHE_DIR, 'license-cache.json');
|
|
|
32
35
|
// Stale limits (ms)
|
|
33
36
|
const LS_CACHE_TTL = 24 * 60 * 60 * 1000; // 24h fresh
|
|
34
37
|
const LS_STALE_LIMIT = 7 * 24 * 60 * 60 * 1000; // 7 days stale max
|
|
35
|
-
const FROGGO_CACHE_TTL = 24 * 60 * 60 * 1000; // 24h fresh
|
|
36
|
-
const FROGGO_STALE_LIMIT = 24 * 60 * 60 * 1000; // 24h stale max
|
|
37
38
|
|
|
38
39
|
// ── Tiers ──────────────────────────────────────────────────────────────────
|
|
39
40
|
|
|
@@ -91,9 +92,18 @@ function writeCache(data) {
|
|
|
91
92
|
try {
|
|
92
93
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
93
94
|
writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2), 'utf8');
|
|
95
|
+
try { chmodSync(CACHE_PATH, 0o600); } catch {}
|
|
94
96
|
} catch { /* best-effort */ }
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
function getTierFromVariantName(variantName) {
|
|
100
|
+
const normalized = (variantName || '').toLowerCase();
|
|
101
|
+
if (!normalized.includes('agency') && !normalized.includes('solo') && !normalized.includes('pro')) {
|
|
102
|
+
console.warn(`[license] Unknown variant name: "${normalized}" — defaulting to solo`);
|
|
103
|
+
}
|
|
104
|
+
return normalized.includes('agency') ? 'agency' : 'solo';
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
/**
|
|
98
108
|
* Check if cached validation is still usable.
|
|
99
109
|
* Returns { valid, tier, stale } or null if cache is expired/missing.
|
|
@@ -103,123 +113,176 @@ function checkCache(key) {
|
|
|
103
113
|
if (!cache || cache.key !== key) return null;
|
|
104
114
|
|
|
105
115
|
const age = Date.now() - (cache.validatedAt || 0);
|
|
106
|
-
const ttl = cache.source === 'froggo' ? FROGGO_CACHE_TTL : LS_CACHE_TTL;
|
|
107
|
-
const staleLimit = cache.source === 'froggo' ? FROGGO_STALE_LIMIT : LS_STALE_LIMIT;
|
|
108
116
|
|
|
109
|
-
if (age <
|
|
110
|
-
return { valid: true, tier: cache.tier, stale: false, source: cache.
|
|
117
|
+
if (age < LS_CACHE_TTL) {
|
|
118
|
+
return { valid: true, tier: cache.tier, stale: false, source: 'lemon-squeezy', instanceId: cache.instanceId };
|
|
111
119
|
}
|
|
112
|
-
if (age <
|
|
113
|
-
return { valid: true, tier: cache.tier, stale: true, source: cache.
|
|
120
|
+
if (age < LS_STALE_LIMIT) {
|
|
121
|
+
return { valid: true, tier: cache.tier, stale: true, source: 'lemon-squeezy', instanceId: cache.instanceId };
|
|
114
122
|
}
|
|
115
123
|
return null; // Expired beyond stale limit
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
// ── Lemon Squeezy
|
|
126
|
+
// ── Lemon Squeezy License API ────────────────────────────────────────────
|
|
119
127
|
|
|
120
128
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
129
|
+
* Activate a license key against Lemon Squeezy.
|
|
130
|
+
* POST https://api.lemonsqueezy.com/v1/licenses/activate
|
|
131
|
+
* Params: license_key, instance_name
|
|
132
|
+
* Returns instance_id on success.
|
|
123
133
|
*/
|
|
124
|
-
async function
|
|
134
|
+
async function activateWithLS(key) {
|
|
125
135
|
try {
|
|
126
136
|
const controller = new AbortController();
|
|
127
137
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
128
138
|
|
|
129
|
-
const
|
|
139
|
+
const body = new URLSearchParams({
|
|
140
|
+
license_key: key,
|
|
141
|
+
instance_name: `seo-intel-${getMachineId()}`,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const res = await fetch('https://api.lemonsqueezy.com/v1/licenses/activate', {
|
|
130
145
|
signal: controller.signal,
|
|
131
146
|
method: 'POST',
|
|
132
|
-
headers: { '
|
|
133
|
-
body
|
|
134
|
-
license_key: key,
|
|
135
|
-
instance_name: `seo-intel-${getMachineId()}`,
|
|
136
|
-
}),
|
|
147
|
+
headers: { 'Accept': 'application/json' },
|
|
148
|
+
body,
|
|
137
149
|
});
|
|
138
150
|
|
|
139
151
|
clearTimeout(timeout);
|
|
140
152
|
const data = await res.json();
|
|
141
153
|
|
|
142
|
-
if (data.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
if (data.activated) {
|
|
155
|
+
const variantName = (data.meta?.variant_name || '').toLowerCase();
|
|
156
|
+
const tier = getTierFromVariantName(variantName);
|
|
157
|
+
return {
|
|
158
|
+
valid: true,
|
|
159
|
+
tier,
|
|
160
|
+
instanceId: data.instance?.id || null,
|
|
161
|
+
meta: data.meta,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Already at activation limit? Try validate instead (might be same machine re-activating)
|
|
166
|
+
if (data.error && data.error.includes('activation limit')) {
|
|
167
|
+
return { valid: false, error: data.error, activationLimitReached: true };
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
return { valid: false, error: data.error || '
|
|
170
|
+
return { valid: false, error: data.error || 'Activation failed' };
|
|
151
171
|
} catch (err) {
|
|
152
172
|
return { valid: false, error: `Network error: ${err.message}`, offline: true };
|
|
153
173
|
}
|
|
154
174
|
}
|
|
155
175
|
|
|
156
|
-
// ── Froggo Token Validation ────────────────────────────────────────────────
|
|
157
|
-
|
|
158
176
|
/**
|
|
159
|
-
* Validate a
|
|
160
|
-
*
|
|
177
|
+
* Validate a license key against Lemon Squeezy.
|
|
178
|
+
* POST https://api.lemonsqueezy.com/v1/licenses/validate
|
|
179
|
+
* Params: license_key, instance_id (optional)
|
|
161
180
|
*/
|
|
162
|
-
async function
|
|
181
|
+
async function validateWithLS(key, instanceId) {
|
|
163
182
|
try {
|
|
164
183
|
const controller = new AbortController();
|
|
165
184
|
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
166
185
|
|
|
167
|
-
const
|
|
186
|
+
const params = { license_key: key };
|
|
187
|
+
if (instanceId) params.instance_id = instanceId;
|
|
188
|
+
const body = new URLSearchParams(params);
|
|
189
|
+
|
|
190
|
+
const res = await fetch('https://api.lemonsqueezy.com/v1/licenses/validate', {
|
|
168
191
|
signal: controller.signal,
|
|
169
192
|
method: 'POST',
|
|
170
|
-
headers: { '
|
|
171
|
-
body
|
|
172
|
-
token,
|
|
173
|
-
product: 'seo-intel',
|
|
174
|
-
}),
|
|
193
|
+
headers: { 'Accept': 'application/json' },
|
|
194
|
+
body,
|
|
175
195
|
});
|
|
176
196
|
|
|
177
197
|
clearTimeout(timeout);
|
|
178
198
|
const data = await res.json();
|
|
179
199
|
|
|
180
200
|
if (data.valid) {
|
|
181
|
-
const
|
|
182
|
-
|
|
201
|
+
const variantName = (data.meta?.variant_name || '').toLowerCase();
|
|
202
|
+
const tier = getTierFromVariantName(variantName);
|
|
203
|
+
return {
|
|
204
|
+
valid: true,
|
|
205
|
+
tier,
|
|
206
|
+
instanceId: data.instance?.id || instanceId,
|
|
207
|
+
status: data.license_key?.status,
|
|
208
|
+
meta: data.meta,
|
|
209
|
+
};
|
|
183
210
|
}
|
|
184
211
|
|
|
185
|
-
return { valid: false, error: data.error || '
|
|
212
|
+
return { valid: false, error: data.error || 'License key not valid', status: data.license_key?.status };
|
|
186
213
|
} catch (err) {
|
|
187
214
|
return { valid: false, error: `Network error: ${err.message}`, offline: true };
|
|
188
215
|
}
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Deactivate a license key instance.
|
|
220
|
+
* POST https://api.lemonsqueezy.com/v1/licenses/deactivate
|
|
221
|
+
* Params: license_key, instance_id
|
|
222
|
+
*/
|
|
223
|
+
export async function deactivateLicense() {
|
|
224
|
+
const keyInfo = readKeyFromEnv();
|
|
225
|
+
if (!keyInfo) return { deactivated: false, error: 'No license key found' };
|
|
226
|
+
|
|
227
|
+
const cache = readCache();
|
|
228
|
+
const instanceId = cache?.instanceId;
|
|
229
|
+
if (!instanceId) return { deactivated: false, error: 'No active instance to deactivate' };
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
234
|
+
|
|
235
|
+
const body = new URLSearchParams({
|
|
236
|
+
license_key: keyInfo.value,
|
|
237
|
+
instance_id: instanceId,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const res = await fetch('https://api.lemonsqueezy.com/v1/licenses/deactivate', {
|
|
241
|
+
signal: controller.signal,
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Accept': 'application/json' },
|
|
244
|
+
body,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
const data = await res.json();
|
|
249
|
+
|
|
250
|
+
if (data.deactivated) {
|
|
251
|
+
// Clear local cache
|
|
252
|
+
writeCache({});
|
|
253
|
+
_cachedLicense = undefined;
|
|
254
|
+
return { deactivated: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { deactivated: false, error: data.error || 'Deactivation failed' };
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return { deactivated: false, error: `Network error: ${err.message}` };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
191
263
|
// ── License Loading ─────────────────────────────────────────────────────────
|
|
192
264
|
|
|
193
265
|
let _cachedLicense = undefined;
|
|
194
266
|
|
|
195
267
|
/**
|
|
196
|
-
* Read the license key
|
|
268
|
+
* Read the license key from environment or .env file.
|
|
197
269
|
* Returns { type, value } or null.
|
|
198
270
|
*/
|
|
199
271
|
function readKeyFromEnv() {
|
|
200
|
-
// 1. Check Froggo token first (marketplace priority)
|
|
201
|
-
let froggoToken = process.env.FROGGO_TOKEN;
|
|
202
272
|
let lsKey = process.env.SEO_INTEL_LICENSE;
|
|
203
273
|
|
|
204
|
-
//
|
|
205
|
-
if (!
|
|
274
|
+
// Check .env file
|
|
275
|
+
if (!lsKey) {
|
|
206
276
|
const envPath = join(ROOT, '.env');
|
|
207
277
|
if (existsSync(envPath)) {
|
|
208
278
|
try {
|
|
209
279
|
const content = readFileSync(envPath, 'utf8');
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (match) froggoToken = match[1].trim().replace(/^["']|["']$/g, '');
|
|
213
|
-
}
|
|
214
|
-
if (!lsKey) {
|
|
215
|
-
const match = content.match(/^SEO_INTEL_LICENSE=(.+)$/m);
|
|
216
|
-
if (match) lsKey = match[1].trim().replace(/^["']|["']$/g, '');
|
|
217
|
-
}
|
|
280
|
+
const match = content.match(/^SEO_INTEL_LICENSE=(.+)$/m);
|
|
281
|
+
if (match) lsKey = match[1].trim().replace(/^["']|["']$/g, '');
|
|
218
282
|
} catch { /* ok */ }
|
|
219
283
|
}
|
|
220
284
|
}
|
|
221
285
|
|
|
222
|
-
if (froggoToken) return { type: 'froggo', value: froggoToken };
|
|
223
286
|
if (lsKey) return { type: 'lemon-squeezy', value: lsKey };
|
|
224
287
|
return null;
|
|
225
288
|
}
|
|
@@ -260,7 +323,13 @@ export function loadLicense() {
|
|
|
260
323
|
}
|
|
261
324
|
|
|
262
325
|
/**
|
|
263
|
-
* Async license activation — validates against
|
|
326
|
+
* Async license activation — validates against Lemon Squeezy and caches result.
|
|
327
|
+
*
|
|
328
|
+
* Flow:
|
|
329
|
+
* 1. If we have a cached instanceId → validate with it
|
|
330
|
+
* 2. If no instanceId → activate (creates new instance)
|
|
331
|
+
* 3. If activation limit reached → validate without instanceId (key-level check)
|
|
332
|
+
*
|
|
264
333
|
* Call this at startup or when loadLicense() returns needsActivation: true.
|
|
265
334
|
* Returns the validated license object.
|
|
266
335
|
*/
|
|
@@ -273,34 +342,46 @@ export async function activateLicense() {
|
|
|
273
342
|
return _cachedLicense;
|
|
274
343
|
}
|
|
275
344
|
|
|
345
|
+
// Check if we already have an instanceId from a previous activation
|
|
346
|
+
const cache = readCache();
|
|
347
|
+
const existingInstanceId = (cache && cache.key === keyInfo.value) ? cache.instanceId : null;
|
|
348
|
+
|
|
276
349
|
let result;
|
|
277
|
-
|
|
278
|
-
|
|
350
|
+
|
|
351
|
+
if (existingInstanceId) {
|
|
352
|
+
// We have a stored instance — validate it
|
|
353
|
+
result = await validateWithLS(keyInfo.value, existingInstanceId);
|
|
279
354
|
} else {
|
|
280
|
-
|
|
355
|
+
// No stored instance — try to activate
|
|
356
|
+
result = await activateWithLS(keyInfo.value);
|
|
357
|
+
|
|
358
|
+
// If activation limit reached, fall back to key-level validate
|
|
359
|
+
if (!result.valid && result.activationLimitReached) {
|
|
360
|
+
result = await validateWithLS(keyInfo.value);
|
|
361
|
+
}
|
|
281
362
|
}
|
|
282
363
|
|
|
283
364
|
if (result.valid) {
|
|
284
|
-
// Cache the successful validation
|
|
365
|
+
// Cache the successful validation with instanceId
|
|
285
366
|
writeCache({
|
|
286
367
|
key: keyInfo.value,
|
|
287
368
|
tier: result.tier,
|
|
288
369
|
validatedAt: Date.now(),
|
|
289
|
-
source:
|
|
370
|
+
source: 'lemon-squeezy',
|
|
371
|
+
instanceId: result.instanceId || existingInstanceId,
|
|
290
372
|
machineId: getMachineId(),
|
|
291
373
|
});
|
|
292
374
|
|
|
293
375
|
const tierData = TIERS[result.tier] || TIERS.solo;
|
|
294
|
-
_cachedLicense = { active: true, tier: result.tier, key: keyInfo.value, source:
|
|
376
|
+
_cachedLicense = { active: true, tier: result.tier, key: keyInfo.value, source: 'lemon-squeezy', ...tierData };
|
|
295
377
|
return _cachedLicense;
|
|
296
378
|
}
|
|
297
379
|
|
|
298
380
|
if (result.offline) {
|
|
299
381
|
// Network error — check if we have any stale cache at all
|
|
300
|
-
const cache = readCache();
|
|
301
382
|
if (cache && cache.key === keyInfo.value) {
|
|
302
383
|
const tierData = TIERS[cache.tier] || TIERS.solo;
|
|
303
|
-
_cachedLicense = { active: true, tier: cache.tier, key: keyInfo.value, source:
|
|
384
|
+
_cachedLicense = { active: true, tier: cache.tier, key: keyInfo.value, source: 'lemon-squeezy', stale: true, ...tierData };
|
|
304
385
|
return _cachedLicense;
|
|
305
386
|
}
|
|
306
387
|
}
|
package/lib/updater.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Two update channels:
|
|
8
8
|
* - npm registry (public installs via `npm install -g seo-intel`)
|
|
9
|
-
* -
|
|
9
|
+
* - ukkometa.fi (direct downloads / pro users)
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* import { checkForUpdates, printUpdateNotice } from './updater.js';
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
20
|
+
import { homedir } from 'os';
|
|
20
21
|
import { join, dirname } from 'path';
|
|
21
22
|
import { fileURLToPath } from 'url';
|
|
22
23
|
|
|
@@ -40,8 +41,8 @@ export function getCurrentVersion() {
|
|
|
40
41
|
|
|
41
42
|
// ── Cache file ─────────────────────────────────────────────────────────────
|
|
42
43
|
|
|
43
|
-
const CACHE_DIR = join(
|
|
44
|
-
const CACHE_FILE = join(CACHE_DIR, 'update-
|
|
44
|
+
const CACHE_DIR = join(homedir(), '.seo-intel');
|
|
45
|
+
const CACHE_FILE = join(CACHE_DIR, 'update-cache.json');
|
|
45
46
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
46
47
|
|
|
47
48
|
function readCache() {
|
|
@@ -113,15 +114,15 @@ async function checkNpm() {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
|
-
* Check
|
|
117
|
+
* Check ukkometa.fi for latest version.
|
|
117
118
|
* Endpoint returns { version, changelog?, downloadUrl? }
|
|
118
119
|
*/
|
|
119
|
-
async function
|
|
120
|
+
async function checkUkkometa() {
|
|
120
121
|
const controller = new AbortController();
|
|
121
122
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
122
123
|
|
|
123
124
|
try {
|
|
124
|
-
const res = await fetch('https://
|
|
125
|
+
const res = await fetch('https://ukkometa.fi/api/seo-intel/version', {
|
|
125
126
|
signal: controller.signal,
|
|
126
127
|
headers: { 'Accept': 'application/json' },
|
|
127
128
|
});
|
|
@@ -165,12 +166,12 @@ export function checkForUpdates() {
|
|
|
165
166
|
const current = getCurrentVersion();
|
|
166
167
|
|
|
167
168
|
// Check both sources in parallel
|
|
168
|
-
const [npmVersion,
|
|
169
|
+
const [npmVersion, ukkometaData] = await Promise.all([
|
|
169
170
|
checkNpm(),
|
|
170
|
-
|
|
171
|
+
checkUkkometa(),
|
|
171
172
|
]);
|
|
172
173
|
|
|
173
|
-
const
|
|
174
|
+
const ukkometaVersion = ukkometaData?.version || null;
|
|
174
175
|
|
|
175
176
|
// Determine the highest available version
|
|
176
177
|
let latestVersion = current;
|
|
@@ -182,11 +183,11 @@ export function checkForUpdates() {
|
|
|
182
183
|
latestVersion = npmVersion;
|
|
183
184
|
source = 'npm';
|
|
184
185
|
}
|
|
185
|
-
if (
|
|
186
|
-
latestVersion =
|
|
187
|
-
source = '
|
|
188
|
-
changelog =
|
|
189
|
-
downloadUrl =
|
|
186
|
+
if (ukkometaVersion && compareSemver(ukkometaVersion, latestVersion) > 0) {
|
|
187
|
+
latestVersion = ukkometaVersion;
|
|
188
|
+
source = 'ukkometa';
|
|
189
|
+
changelog = ukkometaData.changelog;
|
|
190
|
+
downloadUrl = ukkometaData.downloadUrl;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
const hasUpdate = compareSemver(latestVersion, current) > 0;
|
|
@@ -199,10 +200,10 @@ export function checkForUpdates() {
|
|
|
199
200
|
changelog,
|
|
200
201
|
downloadUrl,
|
|
201
202
|
npmVersion,
|
|
202
|
-
|
|
203
|
-
security:
|
|
204
|
-
securitySeverity:
|
|
205
|
-
updatePolicy:
|
|
203
|
+
ukkometaVersion,
|
|
204
|
+
security: ukkometaData?.security || false,
|
|
205
|
+
securitySeverity: ukkometaData?.securitySeverity || null,
|
|
206
|
+
updatePolicy: ukkometaData?.updatePolicy || null,
|
|
206
207
|
};
|
|
207
208
|
|
|
208
209
|
writeCache(_updateResult);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seo-intel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"description": "Local Ahrefs-style SEO competitor intelligence. Crawl → SQLite → cloud analysis.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"Start SEO Intel.command",
|
|
57
57
|
"Setup SEO Intel.command",
|
|
58
58
|
"Start SEO Intel.bat",
|
|
59
|
-
"start-seo-intel.sh"
|
|
59
|
+
"start-seo-intel.sh",
|
|
60
|
+
"CHANGELOG.md"
|
|
60
61
|
],
|
|
61
62
|
"scripts": {
|
|
62
63
|
"crawl": "node cli.js crawl",
|
package/seo-audit.js
CHANGED
|
@@ -507,7 +507,7 @@ async function generateReport(rawUrl) {
|
|
|
507
507
|
|
|
508
508
|
lines.push(`# SEO Audit Report — ${rawUrl}`);
|
|
509
509
|
lines.push(`**Date:** ${date} `);
|
|
510
|
-
lines.push(`**Tool:** SEO Intel (
|
|
510
|
+
lines.push(`**Tool:** SEO Intel (ukkometa.fi)\n`);
|
|
511
511
|
lines.push(`---\n`);
|
|
512
512
|
|
|
513
513
|
// Score card
|
|
@@ -596,7 +596,7 @@ async function generateReport(rawUrl) {
|
|
|
596
596
|
}
|
|
597
597
|
lines.push('');
|
|
598
598
|
lines.push(`---\n`);
|
|
599
|
-
lines.push(`*Generated by SEO Intel —
|
|
599
|
+
lines.push(`*Generated by SEO Intel — ukkometa.fi*`);
|
|
600
600
|
|
|
601
601
|
const md = lines.join('\n');
|
|
602
602
|
writeFileSync(outFile, md);
|
package/server.js
CHANGED
|
@@ -501,13 +501,11 @@ async function handleRequest(req, res) {
|
|
|
501
501
|
envContent = readFileSync(envPath, 'utf8');
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
const isFroggo = key.startsWith('FROGGO_') || key.length > 60;
|
|
506
|
-
const envVar = isFroggo ? 'FROGGO_TOKEN' : 'SEO_INTEL_LICENSE';
|
|
504
|
+
const envVar = 'SEO_INTEL_LICENSE';
|
|
507
505
|
|
|
508
|
-
// Remove existing
|
|
506
|
+
// Remove existing license line
|
|
509
507
|
const lines = envContent.split('\n').filter(l =>
|
|
510
|
-
!l.startsWith('SEO_INTEL_LICENSE=')
|
|
508
|
+
!l.startsWith('SEO_INTEL_LICENSE=')
|
|
511
509
|
);
|
|
512
510
|
lines.push(`${envVar}=${key}`);
|
|
513
511
|
|