jskelet 0.2.2 → 0.2.4
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 +375 -315
- package/docs/03-routing.md +4 -0
- package/docs/06-cache.md +154 -10
- package/docs/07-yapilandirma.md +63 -2
- package/docs/en/03-routing.md +4 -0
- package/docs/en/06-caching.md +154 -11
- package/docs/en/07-configuration.md +65 -2
- package/package.json +1 -1
- package/src/client/cache-panel/i18n.js +670 -0
- package/src/client/cache-panel/login.html +23 -12
- package/src/client/cache-panel/panel.css +314 -6
- package/src/client/cache-panel/panel.html +205 -30
- package/src/client/cache-panel/panel.js +605 -29
- package/src/config/defaults.js +31 -0
- package/src/config/index.js +87 -2
- package/src/index.js +16 -1
- package/src/log.mjs +25 -0
- package/src/server/cache-panel.js +294 -40
- package/src/server/cloudflare.js +595 -0
- package/src/server/redis.js +108 -0
- package/src/server/render.js +74 -2
- package/src/version.mjs +9 -0
package/src/config/defaults.js
CHANGED
|
@@ -215,6 +215,37 @@ export const DEFAULT_CACHE_PANEL = {
|
|
|
215
215
|
sessionHours: 12,
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Cloudflare cache yüzeyi.
|
|
220
|
+
*
|
|
221
|
+
* JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü kopya CDN'de.
|
|
222
|
+
* Bu bölüm ikisini aynı panelden yönetilebilir kılar (bkz.
|
|
223
|
+
* `src/server/cloudflare.js`).
|
|
224
|
+
*
|
|
225
|
+
* Token **config'e yazılmamalı**: `JSKELET_CLOUDFLARE_KEY` env'i önceliklidir
|
|
226
|
+
* ve önerilen yol odur. Zone kimliği sır değil, ama o da env'den okunabilir
|
|
227
|
+
* (`JSKELET_CLOUDFLARE_ZONE_ID`).
|
|
228
|
+
*
|
|
229
|
+
* Gereken token izinleri: purge için `Zone.Cache Purge`, ayarlar için
|
|
230
|
+
* `Zone.Zone Settings`, analitik için `Zone.Analytics` (salt okunur).
|
|
231
|
+
*/
|
|
232
|
+
export const DEFAULT_CLOUDFLARE = {
|
|
233
|
+
/** `false` verilirse env'de token olsa bile yüzey kapalı kalır. */
|
|
234
|
+
enabled: true,
|
|
235
|
+
/** @type {string | null} */
|
|
236
|
+
zoneId: null,
|
|
237
|
+
/** @type {string | null} Env tercih edilir; burada tutmak sırrı repoya sokar. */
|
|
238
|
+
apiToken: null,
|
|
239
|
+
/**
|
|
240
|
+
* Purge, tam URL istiyor; panel elinde yalnızca yol tutuyor. Site adı
|
|
241
|
+
* verilmezse purge isteğinin geldiği istek origin'i kullanılır.
|
|
242
|
+
* @type {string | null}
|
|
243
|
+
*/
|
|
244
|
+
hostname: null,
|
|
245
|
+
/** Analitik penceresi (saat). Cloudflare'in izin verdiği aralıkla sınırlı. */
|
|
246
|
+
analyticsHours: 24,
|
|
247
|
+
};
|
|
248
|
+
|
|
218
249
|
/** Oturuma bağlı sayfalar ısıtılmaz; uygulama kendi listesini verebilir. */
|
|
219
250
|
export const DEFAULT_PREWARM_SKIP = [
|
|
220
251
|
"/api/",
|
package/src/config/index.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* headers() → [{ source, headers: [{ key, value }] }]
|
|
16
16
|
* redirects() → [{ source, destination, permanent?, statusCode? }]
|
|
17
17
|
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
|
-
* cache() → { html?: { [source]: saniye },
|
|
18
|
+
* cache() → { html?: { [source]: saniye },
|
|
19
|
+
* query?: { [source]: string[] | true }, maxEntries?: number,
|
|
19
20
|
* data?: {...}, redis?: {...}, prewarm?: {...},
|
|
20
21
|
* panel?: {...} }
|
|
21
22
|
*
|
|
@@ -30,6 +31,7 @@ import { compilePattern, matchPattern } from "./pattern.js";
|
|
|
30
31
|
import {
|
|
31
32
|
DEFAULT_BRAND,
|
|
32
33
|
DEFAULT_CACHE_PANEL,
|
|
34
|
+
DEFAULT_CLOUDFLARE,
|
|
33
35
|
DEFAULT_DATA_CACHE,
|
|
34
36
|
DEFAULT_DEV_GATE_BYPASS,
|
|
35
37
|
DEFAULT_DIRS,
|
|
@@ -84,6 +86,9 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
84
86
|
* @property {{ pattern: CompiledPattern, destination: string, statusCode: number }[]} redirects
|
|
85
87
|
* @property {{ phase: "beforeFiles" | "afterFiles", pattern: CompiledPattern, destination: string }[]} rewrites
|
|
86
88
|
* @property {{ pattern: CompiledPattern, seconds: number }[]} html
|
|
89
|
+
* @property {{ pattern: CompiledPattern, allow: true | string[] }[]} cacheQuery
|
|
90
|
+
* Yol deseni başına, HTML cache anahtarına girmesine izin verilen query
|
|
91
|
+
* parametreleri. Eşleşen kural yoksa query'li istek cache'lenmez.
|
|
87
92
|
* @property {number} htmlMaxEntries HTML önbelleğinin girdi sınırı.
|
|
88
93
|
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
89
94
|
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
@@ -92,6 +97,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
92
97
|
* @property {RedisConfig} redis Opsiyonel Redis ikinci kademesi.
|
|
93
98
|
* @property {typeof DEFAULT_UPSTREAM_LIMIT} upstream Upstream hız freni.
|
|
94
99
|
* @property {typeof DEFAULT_CACHE_PANEL} cachePanel Önbellek yönetim paneli.
|
|
100
|
+
* @property {typeof DEFAULT_CLOUDFLARE} cloudflare Cloudflare cache yüzeyi.
|
|
95
101
|
* @property {Record<string, unknown>} prewarm
|
|
96
102
|
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
97
103
|
* @property {Record<string, unknown>} brand
|
|
@@ -338,15 +344,87 @@ function normalizeCachePanel(raw) {
|
|
|
338
344
|
};
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Cloudflare bölümü. Token burada da verilebiliyor ama önerilen yol env;
|
|
349
|
+
* normalizasyon sadece tipleri sabitler, sırrı okumak `cloudflare.js`'in işi.
|
|
350
|
+
*
|
|
351
|
+
* @param {unknown} raw
|
|
352
|
+
* @returns {typeof DEFAULT_CLOUDFLARE}
|
|
353
|
+
*/
|
|
354
|
+
function normalizeCloudflare(raw) {
|
|
355
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
356
|
+
const hours = Number(source.analyticsHours);
|
|
357
|
+
|
|
358
|
+
/** @param {unknown} value */
|
|
359
|
+
const text = (value) => (typeof value === "string" && value ? value : null);
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
enabled: source.enabled !== false,
|
|
363
|
+
zoneId: text(source.zoneId),
|
|
364
|
+
apiToken: text(source.apiToken),
|
|
365
|
+
// Şema yazılırsa purge URL'i `https://https://…` olur; baştaki şema atılır.
|
|
366
|
+
hostname: text(source.hostname)?.replace(/^https?:\/\//, "") ?? null,
|
|
367
|
+
analyticsHours:
|
|
368
|
+
Number.isFinite(hours) && hours > 0
|
|
369
|
+
? Math.min(72, Math.floor(hours))
|
|
370
|
+
: DEFAULT_CLOUDFLARE.analyticsHours,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* `cache().query` → yol deseni başına, cache anahtarına girmesine izin verilen
|
|
376
|
+
* query parametreleri.
|
|
377
|
+
*
|
|
378
|
+
* Varsayılan bilinçli olarak "query varsa sayfa dinamik": bir yolun bütün
|
|
379
|
+
* query varyantlarını cache'lemek, `?utm_source=…` gibi sonsuz sayıda anahtar
|
|
380
|
+
* üretip LRU'daki gerçek sayfaları dışarı atıyor. Hangi parametrenin çıktıyı
|
|
381
|
+
* gerçekten değiştirdiğini yalnızca uygulama bilir, o yüzden izin listesi
|
|
382
|
+
* config'ten gelir.
|
|
383
|
+
*
|
|
384
|
+
* Bir desen `true` ile eşlenirse bütün parametreler anahtara girer (eski
|
|
385
|
+
* davranış), `[]` ile eşlenirse hiçbiri girmez — yani query yok sayılır ve
|
|
386
|
+
* bütün varyantlar query'siz sürümün HTML'ini paylaşır.
|
|
387
|
+
*
|
|
388
|
+
* @param {unknown} raw
|
|
389
|
+
* @returns {ResolvedConfig["cacheQuery"]}
|
|
390
|
+
*/
|
|
391
|
+
function normalizeQueryRules(raw) {
|
|
392
|
+
/** @type {ResolvedConfig["cacheQuery"]} */
|
|
393
|
+
const out = [];
|
|
394
|
+
|
|
395
|
+
for (const [source, value] of Object.entries(raw ?? {})) {
|
|
396
|
+
const pattern = compilePattern(source);
|
|
397
|
+
if (!pattern) continue;
|
|
398
|
+
|
|
399
|
+
if (value === true) {
|
|
400
|
+
out.push({ pattern, allow: true });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (value === false) continue;
|
|
404
|
+
|
|
405
|
+
const allow = asArray(
|
|
406
|
+
typeof value === "string" ? [value] : value,
|
|
407
|
+
`cache().query["${source}"]`,
|
|
408
|
+
)
|
|
409
|
+
.filter((name) => typeof name === "string" && name)
|
|
410
|
+
.map(String);
|
|
411
|
+
out.push({ pattern, allow });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
|
|
341
417
|
/**
|
|
342
418
|
* @param {unknown} raw
|
|
343
|
-
* @returns {{ html: ResolvedConfig["html"],
|
|
419
|
+
* @returns {{ html: ResolvedConfig["html"],
|
|
420
|
+
* cacheQuery: ResolvedConfig["cacheQuery"], htmlMaxEntries: number,
|
|
344
421
|
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
345
422
|
* trackDependencies: boolean,
|
|
346
423
|
* transientRetry: { attempts: number, delayMs: number },
|
|
347
424
|
* redis: RedisConfig,
|
|
348
425
|
* upstream: typeof DEFAULT_UPSTREAM_LIMIT,
|
|
349
426
|
* cachePanel: typeof DEFAULT_CACHE_PANEL,
|
|
427
|
+
* cloudflare: typeof DEFAULT_CLOUDFLARE,
|
|
350
428
|
* prewarm: Record<string, unknown>,
|
|
351
429
|
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
352
430
|
*/
|
|
@@ -362,10 +440,12 @@ function normalizeCache(raw) {
|
|
|
362
440
|
}
|
|
363
441
|
|
|
364
442
|
const prewarm = { ...DEFAULT_PREWARM, ...(raw?.prewarm ?? {}) };
|
|
443
|
+
const queryRules = normalizeQueryRules(raw?.query);
|
|
365
444
|
const maxEntries = Number(raw?.maxEntries);
|
|
366
445
|
|
|
367
446
|
return {
|
|
368
447
|
html,
|
|
448
|
+
cacheQuery: queryRules,
|
|
369
449
|
htmlMaxEntries:
|
|
370
450
|
Number.isFinite(maxEntries) && maxEntries > 0
|
|
371
451
|
? Math.floor(maxEntries)
|
|
@@ -385,6 +465,7 @@ function normalizeCache(raw) {
|
|
|
385
465
|
redis: normalizeRedis(raw?.redis),
|
|
386
466
|
upstream: normalizeUpstream(raw?.upstream),
|
|
387
467
|
cachePanel: normalizeCachePanel(raw?.panel),
|
|
468
|
+
cloudflare: normalizeCloudflare(raw?.cloudflare),
|
|
388
469
|
// Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
|
|
389
470
|
// ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
|
|
390
471
|
prewarm,
|
|
@@ -597,6 +678,7 @@ export async function loadConfig(options = {}) {
|
|
|
597
678
|
|
|
598
679
|
const {
|
|
599
680
|
html,
|
|
681
|
+
cacheQuery,
|
|
600
682
|
htmlMaxEntries,
|
|
601
683
|
data,
|
|
602
684
|
trackUpstream,
|
|
@@ -605,6 +687,7 @@ export async function loadConfig(options = {}) {
|
|
|
605
687
|
redis,
|
|
606
688
|
upstream,
|
|
607
689
|
cachePanel,
|
|
690
|
+
cloudflare,
|
|
608
691
|
prewarm,
|
|
609
692
|
prewarmPriority,
|
|
610
693
|
} = normalizeCache(cache);
|
|
@@ -619,6 +702,7 @@ export async function loadConfig(options = {}) {
|
|
|
619
702
|
redirects: normalizeRedirects(redirects),
|
|
620
703
|
rewrites: normalizeRewrites(rewrites),
|
|
621
704
|
html,
|
|
705
|
+
cacheQuery,
|
|
622
706
|
htmlMaxEntries,
|
|
623
707
|
data,
|
|
624
708
|
trackUpstream,
|
|
@@ -627,6 +711,7 @@ export async function loadConfig(options = {}) {
|
|
|
627
711
|
redis,
|
|
628
712
|
upstream,
|
|
629
713
|
cachePanel,
|
|
714
|
+
cloudflare,
|
|
630
715
|
prewarm,
|
|
631
716
|
prewarmPriority,
|
|
632
717
|
brand,
|
package/src/index.js
CHANGED
|
@@ -58,7 +58,22 @@ export {
|
|
|
58
58
|
} from "./server/data-cache.js";
|
|
59
59
|
// Paylaşımlı önbellek kademesinin durumu. Bağlantı kurulmamışken de güvenle
|
|
60
60
|
// çağrılır; healthcheck uçları bunu okuyor.
|
|
61
|
-
|
|
61
|
+
// `getRedisDetails()` bağlantının nereye kurulduğunu ve hangi türleri
|
|
62
|
+
// paylaştığını söyler (şifre asla dönmez); `inspectRedis()` tür başına anahtar
|
|
63
|
+
// sayar — bir `SCAN` turu olduğu için istek yolunda çağrılmamalı.
|
|
64
|
+
export { getRedisDetails, getRedisStatus, inspectRedis } from "./server/redis.js";
|
|
65
|
+
// CDN kademesi. JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü
|
|
66
|
+
// kopya edge'de duruyor. Bir içeriği gerçekten tazelemek için ikisini birlikte
|
|
67
|
+
// düşürmek gerekiyor: `invalidateHtmlCache()` + `purgeCloudflare()`.
|
|
68
|
+
export {
|
|
69
|
+
cloudflareConfigured,
|
|
70
|
+
fetchCacheAnalytics,
|
|
71
|
+
fetchCloudflareOverview,
|
|
72
|
+
fetchPathEdges,
|
|
73
|
+
getCloudflareStatus,
|
|
74
|
+
purgeCloudflare,
|
|
75
|
+
toCloudflareUrls,
|
|
76
|
+
} from "./server/cloudflare.js";
|
|
62
77
|
// Upstream hız freninin host başına durumu: healthcheck ve teşhis uçları için.
|
|
63
78
|
export { getUpstreamLimiterStatus } from "./server/upstream-limiter.js";
|
|
64
79
|
export { prewarm, prewarmProgress } from "./server/prewarm.js";
|
package/src/log.mjs
CHANGED
|
@@ -284,6 +284,31 @@ function truncate(text, max) {
|
|
|
284
284
|
/** Kutunun içi sabit genişlikte; uzun satırlar kırpılır. */
|
|
285
285
|
const BOX = 52;
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Çerçeveli bilgi kutusu.
|
|
289
|
+
*
|
|
290
|
+
* Açılış logunda kaybolmaması gereken tek şey sır: önbellek panelinin şifresi
|
|
291
|
+
* her restart'ta değişiyor ve kullanıcı onu bir kez, akışın içinde görüyor.
|
|
292
|
+
* Genişlik içeriğe göre büyür — bir URL'i ya da 32 haneli bir şifreyi
|
|
293
|
+
* kırpmak kutunun bütün amacını bozar.
|
|
294
|
+
*
|
|
295
|
+
* @param {{ title: string, lines: string[],
|
|
296
|
+
* tint?: (value: string) => string }} info `lines` içinde boş string ayırıcı
|
|
297
|
+
* satır olur. Satırlar **düz metin** olmalı: bir ANSI dizisi `length`e
|
|
298
|
+
* sayıldığı için hizalamayı bozar, rengi çerçeve taşır.
|
|
299
|
+
*/
|
|
300
|
+
export function box({ title, lines, tint = c.cyan }) {
|
|
301
|
+
const width = Math.max(BOX, title.length + 4, ...lines.map((text) => text.length + 4));
|
|
302
|
+
const top = `┌─ ${title} ${"─".repeat(width - title.length - 3)}┐`;
|
|
303
|
+
const bottom = `└${"─".repeat(width)}┘`;
|
|
304
|
+
|
|
305
|
+
write(`\n${tint(top)}\n`);
|
|
306
|
+
for (const text of ["", ...lines, ""]) {
|
|
307
|
+
write(`${tint("│")} ${text}${" ".repeat(width - 4 - text.length)} ${tint("│")}\n`);
|
|
308
|
+
}
|
|
309
|
+
write(`${tint(bottom)}\n\n`);
|
|
310
|
+
}
|
|
311
|
+
|
|
287
312
|
/**
|
|
288
313
|
* Çerçeveli hata kutusu — kendi framework'ünü geliştirirken hatanın
|
|
289
314
|
* akış içinde kaybolmaması için.
|