domma-cms 1.6.2 → 1.6.3
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/package.json
CHANGED
|
@@ -345,6 +345,15 @@ export async function analyticsRoutes(fastify, opts = {}) {
|
|
|
345
345
|
|
|
346
346
|
await migrateLegacy();
|
|
347
347
|
|
|
348
|
+
// Serialise read/modify/write operations so parallel beacons cannot lose
|
|
349
|
+
// counts or race over the same atomic-write temporary file.
|
|
350
|
+
let writeQueue = Promise.resolve();
|
|
351
|
+
const withAnalyticsWrite = (operation) => {
|
|
352
|
+
const result = writeQueue.then(operation);
|
|
353
|
+
writeQueue = result.catch(() => {});
|
|
354
|
+
return result;
|
|
355
|
+
};
|
|
356
|
+
|
|
348
357
|
fastify.decorate('analytics', {
|
|
349
358
|
async getStats({ range = '7d', from, to } = {}) {
|
|
350
359
|
const dates = rangeToDates(range, from, to);
|
|
@@ -408,33 +417,35 @@ export async function analyticsRoutes(fastify, opts = {}) {
|
|
|
408
417
|
}
|
|
409
418
|
}
|
|
410
419
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
420
|
+
return withAnalyticsWrite(async () => {
|
|
421
|
+
const today = todayKey();
|
|
422
|
+
const [lifetime, daily, journeys] = await Promise.all([
|
|
423
|
+
readJson(LIFETIME_FILE, {}),
|
|
424
|
+
readJson(DAILY_FILE, {}),
|
|
425
|
+
readJson(JOURNEYS_FILE, {})
|
|
426
|
+
]);
|
|
417
427
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
428
|
+
lifetime[normalised] = (lifetime[normalised] || 0) + 1;
|
|
429
|
+
if (!daily[today]) daily[today] = {};
|
|
430
|
+
daily[today][normalised] = (daily[today][normalised] || 0) + 1;
|
|
431
|
+
|
|
432
|
+
// Journey event - only if sid is a string of reasonable shape, and under cap
|
|
433
|
+
const safeSid = typeof sid === 'string' && sid.length > 0 && sid.length <= 64 ? sid : null;
|
|
434
|
+
const safeRef = typeof ref === 'string' && ref.length <= 512 ? ref : '';
|
|
435
|
+
if (safeSid) {
|
|
436
|
+
if (!journeys[today]) journeys[today] = [];
|
|
437
|
+
if (shouldRecordJourney(journeys[today])) {
|
|
438
|
+
journeys[today].push({ sid: safeSid, t: Date.now(), url: normalised, ref: safeRef });
|
|
439
|
+
}
|
|
429
440
|
}
|
|
430
|
-
}
|
|
431
441
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
442
|
+
await Promise.all([
|
|
443
|
+
writeJson(LIFETIME_FILE, lifetime),
|
|
444
|
+
writeJson(DAILY_FILE, daily),
|
|
445
|
+
writeJson(JOURNEYS_FILE, journeys)
|
|
446
|
+
]);
|
|
447
|
+
return { ok: true };
|
|
448
|
+
});
|
|
438
449
|
});
|
|
439
450
|
|
|
440
451
|
fastify.get('/stats', { preHandler: [authenticate, requireAdmin] }, async (request) => {
|
|
@@ -544,17 +555,19 @@ export async function analyticsRoutes(fastify, opts = {}) {
|
|
|
544
555
|
});
|
|
545
556
|
|
|
546
557
|
fastify.delete('/stats', { preHandler: [authenticate, requireAdmin] }, async () => {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
+
return withAnalyticsWrite(async () => {
|
|
559
|
+
await Promise.all([
|
|
560
|
+
writeJson(LIFETIME_FILE, {}),
|
|
561
|
+
writeJson(DAILY_FILE, {}),
|
|
562
|
+
writeJson(JOURNEYS_FILE, {})
|
|
563
|
+
]);
|
|
564
|
+
try {
|
|
565
|
+
await fs.unlink(LEGACY_FILE);
|
|
566
|
+
} catch {
|
|
567
|
+
// ignore
|
|
568
|
+
}
|
|
569
|
+
return { ok: true };
|
|
570
|
+
});
|
|
558
571
|
});
|
|
559
572
|
}
|
|
560
573
|
|
|
@@ -1690,6 +1690,8 @@ export function scrubCodeRegions(markdown) {
|
|
|
1690
1690
|
|
|
1691
1691
|
// HTML <pre><code>...</code></pre> blocks (rendered by earlier marked.parse calls inside accordion/carousel etc.)
|
|
1692
1692
|
let scrubbed = markdown.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, placeholder);
|
|
1693
|
+
// Collection bodies may already be HTML: inline examples remain literal too.
|
|
1694
|
+
scrubbed = scrubbed.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, placeholder);
|
|
1693
1695
|
// Fenced code blocks (``` or ~~~, with optional language tag)
|
|
1694
1696
|
scrubbed = scrubbed.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\1[ \t]*$/gm, placeholder);
|
|
1695
1697
|
// Inline code spans (single or multiple backticks, non-greedy)
|
|
@@ -4,8 +4,8 @@ import {isGenuinelyLocal} from './network.js';
|
|
|
4
4
|
export function isBrowserAsset(request) {
|
|
5
5
|
if (!['GET', 'HEAD'].includes(request.method)) return false;
|
|
6
6
|
const path = request.url.split('?')[0];
|
|
7
|
-
return /^\/(?:admin|plugins|dist|public)\//.test(path)
|
|
8
|
-
&& /\.(?:m?js|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|html)$/i.test(path);
|
|
7
|
+
return /^\/(?:admin|plugins|dist|public|media)\//.test(path)
|
|
8
|
+
&& /\.(?:m?js|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|html|pdf|mp4|webm|mp3|wav|ogg)$/i.test(path);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function rateLimitOptions({publicMax = 500, authenticatedMax = 3000} = {}) {
|
|
@@ -28,7 +28,9 @@ export function rateLimitOptions({publicMax = 500, authenticatedMax = 3000} = {}
|
|
|
28
28
|
}
|
|
29
29
|
} catch { /* invalid tokens retain the public limit */ }
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// Page crawls and their background API calls have separate budgets.
|
|
32
|
+
const path = request.url.split('?')[0];
|
|
33
|
+
return path.startsWith('/api/') ? request.ip : `page:${request.ip}`;
|
|
32
34
|
},
|
|
33
35
|
max: request => identities.has(request) ? authenticatedMax : publicMax,
|
|
34
36
|
allowList: request => isGenuinelyLocal(request)
|