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.
@@ -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/",
@@ -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 }, maxEntries?: number,
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"], htmlMaxEntries: number,
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
- export { getRedisStatus } from "./server/redis.js";
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.