seo-intel 1.4.8 → 1.4.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/CHANGELOG.md +12 -0
- package/db/db.js +13 -1
- package/package.json +1 -1
- package/server.js +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.9 (2026-04-10)
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- Fixed arbitrary file write via `--out` query param in dashboard terminal API — write paths now server-controlled only
|
|
7
|
+
- Fixed path traversal in froggo config loader — project names validated to `[a-z0-9_-]`
|
|
8
|
+
- Added project name validation to export and terminal API endpoints
|
|
9
|
+
|
|
10
|
+
### URL Normalization
|
|
11
|
+
- Pages are now normalized before storage: fragments stripped (`/#pricing` → `/`), `index.html` collapsed
|
|
12
|
+
- Internal link targets also normalized for consistent orphan/link analysis
|
|
13
|
+
- Re-crawl to clean up existing fragment duplicates in your database
|
|
14
|
+
|
|
3
15
|
## 1.4.8 (2026-04-10)
|
|
4
16
|
|
|
5
17
|
### Export: own site only, zero competitor bloat
|
package/db/db.js
CHANGED
|
@@ -268,7 +268,19 @@ export function upsertDomain(db, { domain, project, role }) {
|
|
|
268
268
|
`).run(domain, project, role, now, now);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
function normalizePageUrl(rawUrl) {
|
|
272
|
+
try {
|
|
273
|
+
const u = new URL(rawUrl);
|
|
274
|
+
u.hash = ''; // strip fragments (#pricing, #faq, etc.)
|
|
275
|
+
let path = u.pathname;
|
|
276
|
+
path = path.replace(/\/index\.html?$/i, '/'); // /en/index.html → /en/
|
|
277
|
+
u.pathname = path;
|
|
278
|
+
return u.toString();
|
|
279
|
+
} catch { return rawUrl; }
|
|
280
|
+
}
|
|
281
|
+
|
|
271
282
|
export function upsertPage(db, { domainId, url, statusCode, wordCount, loadMs, isIndexable, clickDepth = 0, publishedDate = null, modifiedDate = null, contentHash = null, title = null, metaDesc = null, bodyText = null }) {
|
|
283
|
+
url = normalizePageUrl(url);
|
|
272
284
|
const now = Date.now();
|
|
273
285
|
db.prepare(`
|
|
274
286
|
INSERT INTO pages (domain_id, url, crawled_at, first_seen_at, status_code, word_count, load_ms, is_indexable, click_depth, published_date, modified_date, content_hash, title, meta_desc, body_text)
|
|
@@ -350,7 +362,7 @@ export function insertLinks(db, sourceId, links) {
|
|
|
350
362
|
const stmt = db.prepare(`INSERT INTO links (source_id, target_url, anchor_text, is_internal) VALUES (?, ?, ?, ?)`);
|
|
351
363
|
db.exec('BEGIN');
|
|
352
364
|
try {
|
|
353
|
-
for (const l of links) stmt.run(sourceId, l.url, l.anchor, l.isInternal ? 1 : 0);
|
|
365
|
+
for (const l of links) stmt.run(sourceId, normalizePageUrl(l.url), l.anchor, l.isInternal ? 1 : 0);
|
|
354
366
|
db.exec('COMMIT');
|
|
355
367
|
} catch (e) { db.exec('ROLLBACK'); throw e; }
|
|
356
368
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -596,7 +596,7 @@ async function handleRequest(req, res) {
|
|
|
596
596
|
const format = url.searchParams.get('format') || 'json';
|
|
597
597
|
const profile = url.searchParams.get('profile'); // dev | content | ai-pipeline
|
|
598
598
|
|
|
599
|
-
if (!project) { json(res, 400, { error: '
|
|
599
|
+
if (!project || !/^[a-z0-9_-]+$/i.test(project)) { json(res, 400, { error: 'Invalid project name' }); return; }
|
|
600
600
|
|
|
601
601
|
const { getDb } = await import('./db/db.js');
|
|
602
602
|
const db = getDb(join(__dirname, 'seo-intel.db'));
|
|
@@ -1164,6 +1164,10 @@ async function handleRequest(req, res) {
|
|
|
1164
1164
|
const params = url.searchParams;
|
|
1165
1165
|
const command = params.get('command');
|
|
1166
1166
|
const project = params.get('project') || '';
|
|
1167
|
+
if (project && !/^[a-z0-9_-]+$/i.test(project)) {
|
|
1168
|
+
json(res, 400, { error: 'Invalid project name' });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1167
1171
|
|
|
1168
1172
|
// Whitelist allowed commands
|
|
1169
1173
|
const ALLOWED = ['crawl', 'extract', 'analyze', 'export-actions', 'competitive-actions',
|
|
@@ -1190,7 +1194,7 @@ async function handleRequest(req, res) {
|
|
|
1190
1194
|
if (params.get('type')) args.push('--type', params.get('type'));
|
|
1191
1195
|
if (params.get('limit')) args.push('--limit', params.get('limit'));
|
|
1192
1196
|
if (params.has('raw')) args.push('--raw');
|
|
1193
|
-
|
|
1197
|
+
// --out is NOT passed from dashboard — write paths are server-controlled only (see auto-save below)
|
|
1194
1198
|
|
|
1195
1199
|
// Auto-save exports from dashboard to reports/
|
|
1196
1200
|
const EXPORT_CMDS = ['export-actions', 'suggest-usecases', 'competitive-actions'];
|