jskelet 0.6.3 → 0.6.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/AGENTS.md +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
package/src/server/cloudflare.js
CHANGED
|
@@ -1,607 +1,607 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare'in cache yüzeyi: purge, zone ayarları ve cache analitiği.
|
|
3
|
-
*
|
|
4
|
-
* Neden framework'te: JSkelet'in önbelleği **origin** önbelleği. Bir içerik
|
|
5
|
-
* güncellendiğinde `invalidateHtmlCache()` origin'i tazeliyor ama CDN'de duran
|
|
6
|
-
* kopya TTL'ini bekliyor; ziyaretçinin gördüğü HTML orada. İki katmanı ayrı
|
|
7
|
-
* ayrı yönetmek, purge'ü unutulan bir katman bırakıyor.
|
|
8
|
-
*
|
|
9
|
-
* ## Ne yapılabilir, ne yapılamaz
|
|
10
|
-
*
|
|
11
|
-
* Cloudflare API'sinin **verdiği** şeyler burada var: `purge_everything`, URL /
|
|
12
|
-
* prefix / host / cache-tag ile hedefli purge, cache ile ilgili zone ayarları
|
|
13
|
-
* (cache level, browser TTL, development mode, tiered cache, cache reserve) ve
|
|
14
|
-
* GraphQL analitiğinden cache durumu kırılımı.
|
|
15
|
-
*
|
|
16
|
-
* **Vermediği** iki şey var ve panelde de bu böyle yazıyor:
|
|
17
|
-
*
|
|
18
|
-
* 1. "Şu URL şu an kaç edge'de duruyor" sorusunun cevabı yok. Cloudflare
|
|
19
|
-
* yüzlerce şehirde bağımsız önbellekler tutuyor ve hiçbir uç bir objenin
|
|
20
|
-
* envanterini vermiyor. En yakın cevap **gözlem**: GraphQL'de son N saatte
|
|
21
|
-
* hangi kolodan kaç `hit`/`miss` geldiği. `fetchPathEdges()` bunu döner —
|
|
22
|
-
* "kaç edge'de var" değil, "kaç edge o yolu cache'ten servis etti".
|
|
23
|
-
* 2. Bir edge'i uzaktan ısıtmanın yolu yok. Bir obje ancak o koloya düşen
|
|
24
|
-
* gerçek bir istekle cache'e giriyor; sunucudan seçtiğiniz bir şehri
|
|
25
|
-
* ısıtamazsınız. Pratik karşılığı origin'i ısıtmak (`prewarm`) ve
|
|
26
|
-
* Tiered Cache / Cache Reserve açmak: ilk istek yine o edge'e gidiyor ama
|
|
27
|
-
* arkadaki katman origin'e kadar inmiyor.
|
|
28
|
-
*
|
|
29
|
-
* Sözleşme: bu modüldeki hiçbir fonksiyon fırlatmaz. Token yoksa, ağ düşerse
|
|
30
|
-
* ya da Cloudflare hata dönerse sonuç `{ ok: false, error }` olur ve panel
|
|
31
|
-
* bunu gösterir. Token hiçbir dönüş değerinde yer almaz.
|
|
32
|
-
*/
|
|
33
|
-
import process from "node:process";
|
|
34
|
-
import { getConfig } from "../config/index.js";
|
|
35
|
-
import { DEFAULT_CLOUDFLARE } from "../config/defaults.js";
|
|
36
|
-
|
|
37
|
-
const API = "https://api.cloudflare.com/client/v4";
|
|
38
|
-
const GRAPHQL = "https://api.cloudflare.com/client/v4/graphql";
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Tek bir çağrının en fazla bekletebileceği süre. Panel bir teşhis aracı;
|
|
42
|
-
* Cloudflare yavaşladığında sayfayı süresiz açık tutmasın.
|
|
43
|
-
*/
|
|
44
|
-
const TIMEOUT_MS = 8000;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Purge isteğinde tek seferde gönderilebilecek anahtar sayısı. Cloudflare'in
|
|
48
|
-
* sınırı istek başına 100 (Enterprise'da tek dosya purge'ünde 500); en düşük
|
|
49
|
-
* ortak değeri kullanmak her planda çalışan tek davranış.
|
|
50
|
-
*/
|
|
51
|
-
const PURGE_BATCH = 100;
|
|
52
|
-
|
|
53
|
-
/** Genel bakış birkaç uca gidiyor; panel her açılışta hepsini yenilemesin. */
|
|
54
|
-
const OVERVIEW_TTL_MS = 30_000;
|
|
55
|
-
|
|
56
|
-
/** @type {{ at: number, value: object } | null} */
|
|
57
|
-
let overviewCache = null;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Etkin ayarlar. Token **config'ten ya da env'den** okunur; env öncelikli,
|
|
61
|
-
* çünkü sır config dosyasına yazılmamalı.
|
|
62
|
-
*
|
|
63
|
-
* @returns {{ configured: boolean, zoneId: string | null,
|
|
64
|
-
* token: string | null, source: "env" | "config" | null,
|
|
65
|
-
* hostname: string | null, analyticsHours: number, enabled: boolean }}
|
|
66
|
-
*/
|
|
67
|
-
function settings() {
|
|
68
|
-
/** @type {Record<string, any>} */
|
|
69
|
-
let configured = {};
|
|
70
|
-
try {
|
|
71
|
-
configured = getConfig().cloudflare ?? {};
|
|
72
|
-
} catch {
|
|
73
|
-
configured = { ...DEFAULT_CLOUDFLARE };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const envToken = process.env.JSKELET_CLOUDFLARE_KEY || null;
|
|
77
|
-
const token = envToken ?? configured.apiToken ?? null;
|
|
78
|
-
const zoneId =
|
|
79
|
-
process.env.JSKELET_CLOUDFLARE_ZONE_ID || configured.zoneId || null;
|
|
80
|
-
const hostname =
|
|
81
|
-
process.env.JSKELET_CLOUDFLARE_HOSTNAME || configured.hostname || null;
|
|
82
|
-
|
|
83
|
-
const enabled = configured.enabled !== false;
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
enabled,
|
|
87
|
-
zoneId,
|
|
88
|
-
token,
|
|
89
|
-
source: token ? (envToken ? "env" : "config") : null,
|
|
90
|
-
hostname,
|
|
91
|
-
analyticsHours: Number(configured.analyticsHours) || DEFAULT_CLOUDFLARE.analyticsHours,
|
|
92
|
-
// Zone kimliği olmadan hiçbir uç çağrılamaz; ikisi birlikte "kurulu"
|
|
93
|
-
// sayılmanın koşulu.
|
|
94
|
-
configured: enabled && Boolean(token && zoneId),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** @returns {boolean} */
|
|
99
|
-
export function cloudflareConfigured() {
|
|
100
|
-
return settings().configured;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Panelin gösterdiği kurulum özeti. Token'ın kendisi değil, **nereden
|
|
105
|
-
* geldiği** döner.
|
|
106
|
-
*
|
|
107
|
-
* @returns {{ configured: boolean, enabled: boolean, zoneId: string | null,
|
|
108
|
-
* tokenSource: "env" | "config" | null, hostname: string | null }}
|
|
109
|
-
*/
|
|
110
|
-
export function getCloudflareStatus() {
|
|
111
|
-
const config = settings();
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
configured: config.configured,
|
|
115
|
-
enabled: config.enabled,
|
|
116
|
-
// Zone kimliği sır değil (URL'lerde de geçiyor) ama tamamını basmanın da
|
|
117
|
-
// faydası yok; son dört hane hangi zone'a bağlı olduğunu doğrulamaya yeter.
|
|
118
|
-
zoneId: config.zoneId ? `…${config.zoneId.slice(-4)}` : null,
|
|
119
|
-
tokenSource: config.source,
|
|
120
|
-
hostname: config.hostname,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Cloudflare REST çağrısı. Hata **fırlatılmaz**: teşhis ucu bir arıza anında
|
|
126
|
-
* çağrılıyor ve Cloudflare'in 403'ü paneli düşürmemeli.
|
|
127
|
-
*
|
|
128
|
-
* @param {string} path
|
|
129
|
-
* @param {{ method?: string, body?: unknown }} [options]
|
|
130
|
-
* @returns {Promise<{ ok: boolean, result?: any, error?: string }>}
|
|
131
|
-
*/
|
|
132
|
-
async function call(path, options = {}) {
|
|
133
|
-
const config = settings();
|
|
134
|
-
if (!config.configured) return { ok: false, error: "Cloudflare is not configured" };
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const response = await fetch(`${API}/zones/${config.zoneId}${path}`, {
|
|
138
|
-
method: options.method ?? "GET",
|
|
139
|
-
headers: {
|
|
140
|
-
Authorization: `Bearer ${config.token}`,
|
|
141
|
-
"Content-Type": "application/json",
|
|
142
|
-
},
|
|
143
|
-
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
144
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const payload = await response.json().catch(() => null);
|
|
148
|
-
|
|
149
|
-
if (!response.ok || payload?.success === false) {
|
|
150
|
-
return { ok: false, error: describe(payload, response.status) };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { ok: true, result: payload?.result };
|
|
154
|
-
} catch (error) {
|
|
155
|
-
return { ok: false, error: reason(error) };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* @param {any} payload
|
|
161
|
-
* @param {number} status
|
|
162
|
-
* @returns {string}
|
|
163
|
-
*/
|
|
164
|
-
function describe(payload, status) {
|
|
165
|
-
const first = Array.isArray(payload?.errors) ? payload.errors[0] : null;
|
|
166
|
-
if (first?.message) {
|
|
167
|
-
// Kod da yazılır: 10000 (yetki) ile 1012 (geçersiz zone) çok farklı iki
|
|
168
|
-
// düzeltme demek ve mesajlar birbirine benziyor.
|
|
169
|
-
return first.code ? `${first.message} (${first.code})` : String(first.message);
|
|
170
|
-
}
|
|
171
|
-
return `HTTP ${status}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* @param {unknown} error
|
|
176
|
-
* @returns {string}
|
|
177
|
-
*/
|
|
178
|
-
function reason(error) {
|
|
179
|
-
if (error instanceof Error) {
|
|
180
|
-
return error.name === "TimeoutError" ? `timed out after ${TIMEOUT_MS}ms` : error.message;
|
|
181
|
-
}
|
|
182
|
-
return String(error);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* --------------------------------------------------------------- genel bakış */
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Zone künyesi + cache ile ilgili ayarlar.
|
|
189
|
-
*
|
|
190
|
-
* Ayarlar tek tek değil, tek `GET /settings` turuyla okunur ve ilgili olanlar
|
|
191
|
-
* ayıklanır. Tiered Cache ve Cache Reserve ayrı uçlarda yaşıyor; ikisi de
|
|
192
|
-
* planla sınırlı olduğu için hataları **yok sayılır**, yalnızca `null` olurlar
|
|
193
|
-
* — Free bir zone'da panel yine açılmalı.
|
|
194
|
-
*
|
|
195
|
-
* @param {{ force?: boolean }} [options]
|
|
196
|
-
* @returns {Promise<object>}
|
|
197
|
-
*/
|
|
198
|
-
export async function fetchCloudflareOverview(options = {}) {
|
|
199
|
-
const config = settings();
|
|
200
|
-
if (!config.configured) {
|
|
201
|
-
return { ok: false, status: getCloudflareStatus(), error: "not configured" };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (!options.force && overviewCache && Date.now() - overviewCache.at < OVERVIEW_TTL_MS) {
|
|
205
|
-
return overviewCache.value;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const [zone, zoneSettings, tiered, reserve, regional] = await Promise.all([
|
|
209
|
-
call(""),
|
|
210
|
-
call("/settings"),
|
|
211
|
-
call("/argo/tiered_caching"),
|
|
212
|
-
call("/cache/cache_reserve"),
|
|
213
|
-
call("/cache/regional_tiered_cache"),
|
|
214
|
-
]);
|
|
215
|
-
|
|
216
|
-
if (!zone.ok && !zoneSettings.ok) {
|
|
217
|
-
// İkisi de başarısızsa sorun tek bir uçta değil: token ya da zone yanlış.
|
|
218
|
-
const value = {
|
|
219
|
-
ok: false,
|
|
220
|
-
status: getCloudflareStatus(),
|
|
221
|
-
error: zone.error ?? zoneSettings.error,
|
|
222
|
-
};
|
|
223
|
-
overviewCache = { at: Date.now(), value };
|
|
224
|
-
return value;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const map = new Map(
|
|
228
|
-
(Array.isArray(zoneSettings.result) ? zoneSettings.result : []).map(
|
|
229
|
-
(/** @type {any} */ entry) => [entry.id, entry],
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
/** @param {string} id */
|
|
234
|
-
const setting = (id) => map.get(id) ?? null;
|
|
235
|
-
|
|
236
|
-
const development = setting("development_mode");
|
|
237
|
-
|
|
238
|
-
const value = {
|
|
239
|
-
ok: true,
|
|
240
|
-
fetchedAt: Date.now(),
|
|
241
|
-
status: getCloudflareStatus(),
|
|
242
|
-
zone: zone.ok
|
|
243
|
-
? {
|
|
244
|
-
name: zone.result?.name ?? null,
|
|
245
|
-
plan: zone.result?.plan?.name ?? null,
|
|
246
|
-
status: zone.result?.status ?? null,
|
|
247
|
-
paused: zone.result?.paused === true,
|
|
248
|
-
}
|
|
249
|
-
: null,
|
|
250
|
-
settings: {
|
|
251
|
-
cacheLevel: setting("cache_level")?.value ?? null,
|
|
252
|
-
browserCacheTtl: setting("browser_cache_ttl")?.value ?? null,
|
|
253
|
-
edgeCacheTtl: setting("edge_cache_ttl")?.value ?? null,
|
|
254
|
-
sortQueryString: setting("sort_query_string_for_cache")?.value ?? null,
|
|
255
|
-
alwaysOnline: setting("always_online")?.value ?? null,
|
|
256
|
-
developmentMode: development?.value ?? null,
|
|
257
|
-
// Development mode üç saat sonra kendiliğinden kapanıyor; kalan süre
|
|
258
|
-
// "neden hiçbir şey cache'lenmiyor" sorusunun en sık cevabı.
|
|
259
|
-
developmentModeRemaining: Number(development?.time_remaining) || 0,
|
|
260
|
-
tieredCaching: tiered.ok ? tiered.result?.value ?? null : null,
|
|
261
|
-
cacheReserve: reserve.ok ? reserve.result?.value ?? null : null,
|
|
262
|
-
regionalTieredCache: regional.ok ? regional.result?.value ?? null : null,
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
overviewCache = { at: Date.now(), value };
|
|
267
|
-
return value;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/* ------------------------------------------------------------------ analitik */
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* GraphQL sorgusu. Analitik ayrı bir uçta (`/graphql`) yaşadığı için `call()`
|
|
274
|
-
* kullanılmıyor.
|
|
275
|
-
*
|
|
276
|
-
* @param {string} query
|
|
277
|
-
* @param {Record<string, unknown>} variables
|
|
278
|
-
* @returns {Promise<{ ok: boolean, data?: any, error?: string }>}
|
|
279
|
-
*/
|
|
280
|
-
async function graphql(query, variables) {
|
|
281
|
-
const config = settings();
|
|
282
|
-
if (!config.configured) return { ok: false, error: "Cloudflare is not configured" };
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
const response = await fetch(GRAPHQL, {
|
|
286
|
-
method: "POST",
|
|
287
|
-
headers: {
|
|
288
|
-
Authorization: `Bearer ${config.token}`,
|
|
289
|
-
"Content-Type": "application/json",
|
|
290
|
-
},
|
|
291
|
-
body: JSON.stringify({ query, variables }),
|
|
292
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const payload = await response.json().catch(() => null);
|
|
296
|
-
const error = Array.isArray(payload?.errors) ? payload.errors[0]?.message : null;
|
|
297
|
-
|
|
298
|
-
if (!response.ok || error) {
|
|
299
|
-
return { ok: false, error: error ?? `HTTP ${response.status}` };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return { ok: true, data: payload?.data };
|
|
303
|
-
} catch (error) {
|
|
304
|
-
return { ok: false, error: reason(error) };
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* GraphQL analitik penceresinin iki ucu. Yalnızca `datetime_geq` gönderilirse
|
|
310
|
-
* Cloudflare üst sınırı sorguyu işlediği ana koyuyor; Free zone'larda tavan
|
|
311
|
-
* tam 1 gün ve ağ gecikmesi `1d179ms…` gibi bir taşma üretip reddediyor.
|
|
312
|
-
* İki uç aynı `Date.now()`'dan hesaplanınca pencere tam N saat kalır.
|
|
313
|
-
*
|
|
314
|
-
* @param {number} hours
|
|
315
|
-
* @returns {{ since: string, until: string }}
|
|
316
|
-
*/
|
|
317
|
-
function timeWindow(hours) {
|
|
318
|
-
const untilMs = Date.now();
|
|
319
|
-
return {
|
|
320
|
-
since: new Date(untilMs - hours * 3600_000).toISOString(),
|
|
321
|
-
until: new Date(untilMs).toISOString(),
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Zone'un cache durumu kırılımı: `hit`, `miss`, `dynamic`, `expired`… her biri
|
|
327
|
-
* için istek sayısı ve edge'den giden bayt.
|
|
328
|
-
*
|
|
329
|
-
* `httpRequestsAdaptiveGroups` bilinçli seçildi: `cacheStatus` boyutu eski
|
|
330
|
-
* `httpRequests1hGroups` veri kümesinde yok ve pek çok hesapta hata veriyor.
|
|
331
|
-
* Adaptive küme örnekleme kullanıyor, yani sayılar **tahmindir** — oranlar
|
|
332
|
-
* doğru, mutlak değerler yaklaşık.
|
|
333
|
-
*
|
|
334
|
-
* @param {{ hours?: number }} [options]
|
|
335
|
-
* @returns {Promise<{ ok: boolean, hours: number, sampled: boolean,
|
|
336
|
-
* rows: { status: string, requests: number, bytes: number }[],
|
|
337
|
-
* error?: string }>}
|
|
338
|
-
*/
|
|
339
|
-
export async function fetchCacheAnalytics(options = {}) {
|
|
340
|
-
const config = settings();
|
|
341
|
-
const hours = Math.max(1, Math.min(72, options.hours ?? config.analyticsHours));
|
|
342
|
-
const { since, until } = timeWindow(hours);
|
|
343
|
-
|
|
344
|
-
const query = `
|
|
345
|
-
query CacheStatus($zone: String!, $since: Time!, $until: Time!) {
|
|
346
|
-
viewer {
|
|
347
|
-
zones(filter: { zoneTag: $zone }) {
|
|
348
|
-
httpRequestsAdaptiveGroups(
|
|
349
|
-
filter: { datetime_geq: $since, datetime_leq: $until }
|
|
350
|
-
limit: 50
|
|
351
|
-
orderBy: [count_DESC]
|
|
352
|
-
) {
|
|
353
|
-
count
|
|
354
|
-
sum { edgeResponseBytes }
|
|
355
|
-
dimensions { cacheStatus }
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}`;
|
|
360
|
-
|
|
361
|
-
const response = await graphql(query, { zone: config.zoneId, since, until });
|
|
362
|
-
if (!response.ok) return { ok: false, hours, sampled: true, rows: [], error: response.error };
|
|
363
|
-
|
|
364
|
-
const groups = response.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups ?? [];
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
ok: true,
|
|
368
|
-
hours,
|
|
369
|
-
sampled: true,
|
|
370
|
-
rows: groups.map((/** @type {any} */ group) => ({
|
|
371
|
-
status: group.dimensions?.cacheStatus ?? "unknown",
|
|
372
|
-
requests: Number(group.count) || 0,
|
|
373
|
-
bytes: Number(group.sum?.edgeResponseBytes) || 0,
|
|
374
|
-
})),
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Bir yolun **kolo bazında** cache kırılımı.
|
|
380
|
-
*
|
|
381
|
-
* Bu, "kaç edge'de bu sayfanın kopyası var" sorusunun API'nin verdiği en yakın
|
|
382
|
-
* cevabı: son `hours` saatte hangi Cloudflare şehri o yolu kaç kez cache'ten
|
|
383
|
-
* (`hit`) ya da origin'den (`miss`, `expired`, `dynamic`) servis etti. Envanter
|
|
384
|
-
* değil, gözlem — hiç istek almamış bir edge listede görünmez, kopyası olsa da.
|
|
385
|
-
*
|
|
386
|
-
* @param {{ path: string, hours?: number }} options
|
|
387
|
-
* @returns {Promise<{ ok: boolean, path: string, hours: number,
|
|
388
|
-
* colos: { colo: string, hits: number, misses: number }[],
|
|
389
|
-
* hits: number, misses: number, error?: string }>}
|
|
390
|
-
*/
|
|
391
|
-
export async function fetchPathEdges({ path, hours }) {
|
|
392
|
-
const config = settings();
|
|
393
|
-
const window = Math.max(1, Math.min(72, hours ?? config.analyticsHours));
|
|
394
|
-
const { since, until } = timeWindow(window);
|
|
395
|
-
|
|
396
|
-
const query = `
|
|
397
|
-
query PathEdges($zone: String!, $since: Time!, $until: Time!, $path: String!) {
|
|
398
|
-
viewer {
|
|
399
|
-
zones(filter: { zoneTag: $zone }) {
|
|
400
|
-
httpRequestsAdaptiveGroups(
|
|
401
|
-
filter: { datetime_geq: $since, datetime_leq: $until, clientRequestPath: $path }
|
|
402
|
-
limit: 500
|
|
403
|
-
orderBy: [count_DESC]
|
|
404
|
-
) {
|
|
405
|
-
count
|
|
406
|
-
dimensions { coloCode cacheStatus }
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}`;
|
|
411
|
-
|
|
412
|
-
const response = await graphql(query, {
|
|
413
|
-
zone: config.zoneId,
|
|
414
|
-
since,
|
|
415
|
-
until,
|
|
416
|
-
path,
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
if (!response.ok) {
|
|
420
|
-
return { ok: false, path, hours: window, colos: [], hits: 0, misses: 0, error: response.error };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const groups = response.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups ?? [];
|
|
424
|
-
|
|
425
|
-
/** @type {Map<string, { colo: string, hits: number, misses: number }>} */
|
|
426
|
-
const colos = new Map();
|
|
427
|
-
let hits = 0;
|
|
428
|
-
let misses = 0;
|
|
429
|
-
|
|
430
|
-
for (const group of groups) {
|
|
431
|
-
const colo = group.dimensions?.coloCode ?? "??";
|
|
432
|
-
const status = String(group.dimensions?.cacheStatus ?? "");
|
|
433
|
-
const count = Number(group.count) || 0;
|
|
434
|
-
|
|
435
|
-
const entry = colos.get(colo) ?? { colo, hits: 0, misses: 0 };
|
|
436
|
-
// `hit` dışındaki her şey (miss, expired, dynamic, bypass) origin'e giden
|
|
437
|
-
// ya da hiç cache'lenmeyen istek; panelde tek kolonda toplanıyor.
|
|
438
|
-
if (status === "hit") {
|
|
439
|
-
entry.hits += count;
|
|
440
|
-
hits += count;
|
|
441
|
-
} else {
|
|
442
|
-
entry.misses += count;
|
|
443
|
-
misses += count;
|
|
444
|
-
}
|
|
445
|
-
colos.set(colo, entry);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
ok: true,
|
|
450
|
-
path,
|
|
451
|
-
hours: window,
|
|
452
|
-
hits,
|
|
453
|
-
misses,
|
|
454
|
-
colos: [...colos.values()].sort((a, b) => b.hits + b.misses - (a.hits + a.misses)),
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/* --------------------------------------------------------------------- purge */
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Cloudflare önbelleğini düşürür.
|
|
462
|
-
*
|
|
463
|
-
* Dört hedefleme biçimi de API'nin verdiği hâliyle: `files` (tam URL),
|
|
464
|
-
* `prefixes`, `hosts`, `tags`. Hepsi artık her planda çalışıyor ama istek
|
|
465
|
-
* başına en fazla 100 anahtar kabul ediliyor; uzun listeler burada parçalanır
|
|
466
|
-
* ve **sırayla** gönderilir — paralel göndermek Free planda dakikada beş
|
|
467
|
-
* istekle sınırlı olan hız freni yüzünden yarısı reddedilen bir tur demek.
|
|
468
|
-
*
|
|
469
|
-
* @param {{ everything?: boolean, files?: string[], prefixes?: string[],
|
|
470
|
-
* hosts?: string[], tags?: string[] }} target
|
|
471
|
-
* @returns {Promise<{ ok: boolean, purged: number, batches: number,
|
|
472
|
-
* error?: string }>}
|
|
473
|
-
*/
|
|
474
|
-
export async function purgeCloudflare(target) {
|
|
475
|
-
if (target.everything) {
|
|
476
|
-
const response = await call("/purge_cache", {
|
|
477
|
-
method: "POST",
|
|
478
|
-
body: { purge_everything: true },
|
|
479
|
-
});
|
|
480
|
-
return { ok: response.ok, purged: 0, batches: 1, error: response.error };
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** @type {[string, string[]][]} */
|
|
484
|
-
const kinds = [
|
|
485
|
-
["files", target.files ?? []],
|
|
486
|
-
["prefixes", target.prefixes ?? []],
|
|
487
|
-
["hosts", target.hosts ?? []],
|
|
488
|
-
["tags", target.tags ?? []],
|
|
489
|
-
];
|
|
490
|
-
|
|
491
|
-
let purged = 0;
|
|
492
|
-
let batches = 0;
|
|
493
|
-
|
|
494
|
-
for (const [kind, values] of kinds) {
|
|
495
|
-
const unique = [...new Set(values.filter((value) => typeof value === "string" && value))];
|
|
496
|
-
|
|
497
|
-
for (let index = 0; index < unique.length; index += PURGE_BATCH) {
|
|
498
|
-
const slice = unique.slice(index, index + PURGE_BATCH);
|
|
499
|
-
const response = await call("/purge_cache", {
|
|
500
|
-
method: "POST",
|
|
501
|
-
body: { [kind]: slice },
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
batches += 1;
|
|
505
|
-
if (!response.ok) return { ok: false, purged, batches, error: response.error };
|
|
506
|
-
purged += slice.length;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (!batches) return { ok: false, purged: 0, batches: 0, error: "nothing to purge" };
|
|
511
|
-
return { ok: true, purged, batches };
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Yolları tam URL'e çevirir. Cloudflare `files` purge'ü şemayı da içeren tam
|
|
516
|
-
* URL istiyor; panel elinde yol (`/haber/abc`) tutuyor.
|
|
517
|
-
*
|
|
518
|
-
* @param {string[]} paths
|
|
519
|
-
* @param {string} [fallbackOrigin] `cloudflare.hostname` verilmediğinde
|
|
520
|
-
* kullanılacak kök (panelin açıldığı origin).
|
|
521
|
-
* @returns {string[]}
|
|
522
|
-
*/
|
|
523
|
-
export function toCloudflareUrls(paths, fallbackOrigin) {
|
|
524
|
-
// Yapılandırılmış site adı önce gelir: panel bir iç adresten (ör.
|
|
525
|
-
// `http://10.0.0.4:3000`) açılmış olabilir ve o adresin Cloudflare'de
|
|
526
|
-
// karşılığı yok.
|
|
527
|
-
const hostname = settings().hostname;
|
|
528
|
-
const base = hostname ? `https://${hostname}` : (fallbackOrigin ?? null);
|
|
529
|
-
if (!base) return [];
|
|
530
|
-
|
|
531
|
-
return paths
|
|
532
|
-
.map((path) => {
|
|
533
|
-
try {
|
|
534
|
-
return new URL(path, base).href;
|
|
535
|
-
} catch {
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
})
|
|
539
|
-
.filter((url) => url !== null);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/* ------------------------------------------------------------------- ayarlar */
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Cache ile ilgili bir zone ayarını değiştirir.
|
|
546
|
-
*
|
|
547
|
-
* Yalnızca beyaz listedeki ayarlar: panelin işi cache, ve bir teşhis
|
|
548
|
-
* arayüzünden zone'un tamamını yeniden yapılandırılabilir yapmak gereksiz bir
|
|
549
|
-
* yüzey.
|
|
550
|
-
*
|
|
551
|
-
* @param {"development_mode" | "cache_level" | "browser_cache_ttl" |
|
|
552
|
-
* "always_online" | "sort_query_string_for_cache"} id
|
|
553
|
-
* @param {string | number} value
|
|
554
|
-
* @returns {Promise<{ ok: boolean, value?: unknown, error?: string }>}
|
|
555
|
-
*/
|
|
556
|
-
export async function setCloudflareSetting(id, value) {
|
|
557
|
-
const allowed = new Set([
|
|
558
|
-
"development_mode",
|
|
559
|
-
"cache_level",
|
|
560
|
-
"browser_cache_ttl",
|
|
561
|
-
"always_online",
|
|
562
|
-
"sort_query_string_for_cache",
|
|
563
|
-
]);
|
|
564
|
-
|
|
565
|
-
if (!allowed.has(id)) return { ok: false, error: `setting not allowed: ${id}` };
|
|
566
|
-
|
|
567
|
-
const response = await call(`/settings/${id}`, { method: "PATCH", body: { value } });
|
|
568
|
-
// Ayar değiştiyse önbelleğe alınmış genel bakış eskidi.
|
|
569
|
-
overviewCache = null;
|
|
570
|
-
|
|
571
|
-
return { ok: response.ok, value: response.result?.value, error: response.error };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Tiered Cache ya da Cache Reserve'ü açar/kapatır. İkisi de ayrı uçlarda ve
|
|
576
|
-
* planla sınırlı; kapalı bir planda Cloudflare açıklayıcı bir hata döner ve
|
|
577
|
-
* panel onu gösterir.
|
|
578
|
-
*
|
|
579
|
-
* @param {"tiered_caching" | "cache_reserve" | "regional_tiered_cache"} feature
|
|
580
|
-
* @param {"on" | "off"} value
|
|
581
|
-
* @returns {Promise<{ ok: boolean, error?: string }>}
|
|
582
|
-
*/
|
|
583
|
-
export async function setCloudflareFeature(feature, value) {
|
|
584
|
-
const path =
|
|
585
|
-
feature === "tiered_caching"
|
|
586
|
-
? "/argo/tiered_caching"
|
|
587
|
-
: feature === "cache_reserve"
|
|
588
|
-
? "/cache/cache_reserve"
|
|
589
|
-
: "/cache/regional_tiered_cache";
|
|
590
|
-
|
|
591
|
-
const response = await call(path, { method: "PATCH", body: { value } });
|
|
592
|
-
overviewCache = null;
|
|
593
|
-
|
|
594
|
-
return { ok: response.ok, error: response.error };
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Cache Reserve'ü (R2'deki kalıcı kopya) boşaltır. Purge'den ayrı bir işlem:
|
|
599
|
-
* `purge_everything` edge önbelleklerini düşürüyor, Cache Reserve'deki kopya
|
|
600
|
-
* kalıyor.
|
|
601
|
-
*
|
|
602
|
-
* @returns {Promise<{ ok: boolean, error?: string }>}
|
|
603
|
-
*/
|
|
604
|
-
export async function clearCloudflareCacheReserve() {
|
|
605
|
-
const response = await call("/cache/cache_reserve_clear", { method: "POST" });
|
|
606
|
-
return { ok: response.ok, error: response.error };
|
|
607
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare'in cache yüzeyi: purge, zone ayarları ve cache analitiği.
|
|
3
|
+
*
|
|
4
|
+
* Neden framework'te: JSkelet'in önbelleği **origin** önbelleği. Bir içerik
|
|
5
|
+
* güncellendiğinde `invalidateHtmlCache()` origin'i tazeliyor ama CDN'de duran
|
|
6
|
+
* kopya TTL'ini bekliyor; ziyaretçinin gördüğü HTML orada. İki katmanı ayrı
|
|
7
|
+
* ayrı yönetmek, purge'ü unutulan bir katman bırakıyor.
|
|
8
|
+
*
|
|
9
|
+
* ## Ne yapılabilir, ne yapılamaz
|
|
10
|
+
*
|
|
11
|
+
* Cloudflare API'sinin **verdiği** şeyler burada var: `purge_everything`, URL /
|
|
12
|
+
* prefix / host / cache-tag ile hedefli purge, cache ile ilgili zone ayarları
|
|
13
|
+
* (cache level, browser TTL, development mode, tiered cache, cache reserve) ve
|
|
14
|
+
* GraphQL analitiğinden cache durumu kırılımı.
|
|
15
|
+
*
|
|
16
|
+
* **Vermediği** iki şey var ve panelde de bu böyle yazıyor:
|
|
17
|
+
*
|
|
18
|
+
* 1. "Şu URL şu an kaç edge'de duruyor" sorusunun cevabı yok. Cloudflare
|
|
19
|
+
* yüzlerce şehirde bağımsız önbellekler tutuyor ve hiçbir uç bir objenin
|
|
20
|
+
* envanterini vermiyor. En yakın cevap **gözlem**: GraphQL'de son N saatte
|
|
21
|
+
* hangi kolodan kaç `hit`/`miss` geldiği. `fetchPathEdges()` bunu döner —
|
|
22
|
+
* "kaç edge'de var" değil, "kaç edge o yolu cache'ten servis etti".
|
|
23
|
+
* 2. Bir edge'i uzaktan ısıtmanın yolu yok. Bir obje ancak o koloya düşen
|
|
24
|
+
* gerçek bir istekle cache'e giriyor; sunucudan seçtiğiniz bir şehri
|
|
25
|
+
* ısıtamazsınız. Pratik karşılığı origin'i ısıtmak (`prewarm`) ve
|
|
26
|
+
* Tiered Cache / Cache Reserve açmak: ilk istek yine o edge'e gidiyor ama
|
|
27
|
+
* arkadaki katman origin'e kadar inmiyor.
|
|
28
|
+
*
|
|
29
|
+
* Sözleşme: bu modüldeki hiçbir fonksiyon fırlatmaz. Token yoksa, ağ düşerse
|
|
30
|
+
* ya da Cloudflare hata dönerse sonuç `{ ok: false, error }` olur ve panel
|
|
31
|
+
* bunu gösterir. Token hiçbir dönüş değerinde yer almaz.
|
|
32
|
+
*/
|
|
33
|
+
import process from "node:process";
|
|
34
|
+
import { getConfig } from "../config/index.js";
|
|
35
|
+
import { DEFAULT_CLOUDFLARE } from "../config/defaults.js";
|
|
36
|
+
|
|
37
|
+
const API = "https://api.cloudflare.com/client/v4";
|
|
38
|
+
const GRAPHQL = "https://api.cloudflare.com/client/v4/graphql";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tek bir çağrının en fazla bekletebileceği süre. Panel bir teşhis aracı;
|
|
42
|
+
* Cloudflare yavaşladığında sayfayı süresiz açık tutmasın.
|
|
43
|
+
*/
|
|
44
|
+
const TIMEOUT_MS = 8000;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Purge isteğinde tek seferde gönderilebilecek anahtar sayısı. Cloudflare'in
|
|
48
|
+
* sınırı istek başına 100 (Enterprise'da tek dosya purge'ünde 500); en düşük
|
|
49
|
+
* ortak değeri kullanmak her planda çalışan tek davranış.
|
|
50
|
+
*/
|
|
51
|
+
const PURGE_BATCH = 100;
|
|
52
|
+
|
|
53
|
+
/** Genel bakış birkaç uca gidiyor; panel her açılışta hepsini yenilemesin. */
|
|
54
|
+
const OVERVIEW_TTL_MS = 30_000;
|
|
55
|
+
|
|
56
|
+
/** @type {{ at: number, value: object } | null} */
|
|
57
|
+
let overviewCache = null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Etkin ayarlar. Token **config'ten ya da env'den** okunur; env öncelikli,
|
|
61
|
+
* çünkü sır config dosyasına yazılmamalı.
|
|
62
|
+
*
|
|
63
|
+
* @returns {{ configured: boolean, zoneId: string | null,
|
|
64
|
+
* token: string | null, source: "env" | "config" | null,
|
|
65
|
+
* hostname: string | null, analyticsHours: number, enabled: boolean }}
|
|
66
|
+
*/
|
|
67
|
+
function settings() {
|
|
68
|
+
/** @type {Record<string, any>} */
|
|
69
|
+
let configured = {};
|
|
70
|
+
try {
|
|
71
|
+
configured = getConfig().cloudflare ?? {};
|
|
72
|
+
} catch {
|
|
73
|
+
configured = { ...DEFAULT_CLOUDFLARE };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const envToken = process.env.JSKELET_CLOUDFLARE_KEY || null;
|
|
77
|
+
const token = envToken ?? configured.apiToken ?? null;
|
|
78
|
+
const zoneId =
|
|
79
|
+
process.env.JSKELET_CLOUDFLARE_ZONE_ID || configured.zoneId || null;
|
|
80
|
+
const hostname =
|
|
81
|
+
process.env.JSKELET_CLOUDFLARE_HOSTNAME || configured.hostname || null;
|
|
82
|
+
|
|
83
|
+
const enabled = configured.enabled !== false;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
enabled,
|
|
87
|
+
zoneId,
|
|
88
|
+
token,
|
|
89
|
+
source: token ? (envToken ? "env" : "config") : null,
|
|
90
|
+
hostname,
|
|
91
|
+
analyticsHours: Number(configured.analyticsHours) || DEFAULT_CLOUDFLARE.analyticsHours,
|
|
92
|
+
// Zone kimliği olmadan hiçbir uç çağrılamaz; ikisi birlikte "kurulu"
|
|
93
|
+
// sayılmanın koşulu.
|
|
94
|
+
configured: enabled && Boolean(token && zoneId),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @returns {boolean} */
|
|
99
|
+
export function cloudflareConfigured() {
|
|
100
|
+
return settings().configured;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Panelin gösterdiği kurulum özeti. Token'ın kendisi değil, **nereden
|
|
105
|
+
* geldiği** döner.
|
|
106
|
+
*
|
|
107
|
+
* @returns {{ configured: boolean, enabled: boolean, zoneId: string | null,
|
|
108
|
+
* tokenSource: "env" | "config" | null, hostname: string | null }}
|
|
109
|
+
*/
|
|
110
|
+
export function getCloudflareStatus() {
|
|
111
|
+
const config = settings();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
configured: config.configured,
|
|
115
|
+
enabled: config.enabled,
|
|
116
|
+
// Zone kimliği sır değil (URL'lerde de geçiyor) ama tamamını basmanın da
|
|
117
|
+
// faydası yok; son dört hane hangi zone'a bağlı olduğunu doğrulamaya yeter.
|
|
118
|
+
zoneId: config.zoneId ? `…${config.zoneId.slice(-4)}` : null,
|
|
119
|
+
tokenSource: config.source,
|
|
120
|
+
hostname: config.hostname,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cloudflare REST çağrısı. Hata **fırlatılmaz**: teşhis ucu bir arıza anında
|
|
126
|
+
* çağrılıyor ve Cloudflare'in 403'ü paneli düşürmemeli.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} path
|
|
129
|
+
* @param {{ method?: string, body?: unknown }} [options]
|
|
130
|
+
* @returns {Promise<{ ok: boolean, result?: any, error?: string }>}
|
|
131
|
+
*/
|
|
132
|
+
async function call(path, options = {}) {
|
|
133
|
+
const config = settings();
|
|
134
|
+
if (!config.configured) return { ok: false, error: "Cloudflare is not configured" };
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(`${API}/zones/${config.zoneId}${path}`, {
|
|
138
|
+
method: options.method ?? "GET",
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${config.token}`,
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
},
|
|
143
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
144
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const payload = await response.json().catch(() => null);
|
|
148
|
+
|
|
149
|
+
if (!response.ok || payload?.success === false) {
|
|
150
|
+
return { ok: false, error: describe(payload, response.status) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { ok: true, result: payload?.result };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return { ok: false, error: reason(error) };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {any} payload
|
|
161
|
+
* @param {number} status
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
function describe(payload, status) {
|
|
165
|
+
const first = Array.isArray(payload?.errors) ? payload.errors[0] : null;
|
|
166
|
+
if (first?.message) {
|
|
167
|
+
// Kod da yazılır: 10000 (yetki) ile 1012 (geçersiz zone) çok farklı iki
|
|
168
|
+
// düzeltme demek ve mesajlar birbirine benziyor.
|
|
169
|
+
return first.code ? `${first.message} (${first.code})` : String(first.message);
|
|
170
|
+
}
|
|
171
|
+
return `HTTP ${status}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {unknown} error
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
function reason(error) {
|
|
179
|
+
if (error instanceof Error) {
|
|
180
|
+
return error.name === "TimeoutError" ? `timed out after ${TIMEOUT_MS}ms` : error.message;
|
|
181
|
+
}
|
|
182
|
+
return String(error);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* --------------------------------------------------------------- genel bakış */
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Zone künyesi + cache ile ilgili ayarlar.
|
|
189
|
+
*
|
|
190
|
+
* Ayarlar tek tek değil, tek `GET /settings` turuyla okunur ve ilgili olanlar
|
|
191
|
+
* ayıklanır. Tiered Cache ve Cache Reserve ayrı uçlarda yaşıyor; ikisi de
|
|
192
|
+
* planla sınırlı olduğu için hataları **yok sayılır**, yalnızca `null` olurlar
|
|
193
|
+
* — Free bir zone'da panel yine açılmalı.
|
|
194
|
+
*
|
|
195
|
+
* @param {{ force?: boolean }} [options]
|
|
196
|
+
* @returns {Promise<object>}
|
|
197
|
+
*/
|
|
198
|
+
export async function fetchCloudflareOverview(options = {}) {
|
|
199
|
+
const config = settings();
|
|
200
|
+
if (!config.configured) {
|
|
201
|
+
return { ok: false, status: getCloudflareStatus(), error: "not configured" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!options.force && overviewCache && Date.now() - overviewCache.at < OVERVIEW_TTL_MS) {
|
|
205
|
+
return overviewCache.value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const [zone, zoneSettings, tiered, reserve, regional] = await Promise.all([
|
|
209
|
+
call(""),
|
|
210
|
+
call("/settings"),
|
|
211
|
+
call("/argo/tiered_caching"),
|
|
212
|
+
call("/cache/cache_reserve"),
|
|
213
|
+
call("/cache/regional_tiered_cache"),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
if (!zone.ok && !zoneSettings.ok) {
|
|
217
|
+
// İkisi de başarısızsa sorun tek bir uçta değil: token ya da zone yanlış.
|
|
218
|
+
const value = {
|
|
219
|
+
ok: false,
|
|
220
|
+
status: getCloudflareStatus(),
|
|
221
|
+
error: zone.error ?? zoneSettings.error,
|
|
222
|
+
};
|
|
223
|
+
overviewCache = { at: Date.now(), value };
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const map = new Map(
|
|
228
|
+
(Array.isArray(zoneSettings.result) ? zoneSettings.result : []).map(
|
|
229
|
+
(/** @type {any} */ entry) => [entry.id, entry],
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
/** @param {string} id */
|
|
234
|
+
const setting = (id) => map.get(id) ?? null;
|
|
235
|
+
|
|
236
|
+
const development = setting("development_mode");
|
|
237
|
+
|
|
238
|
+
const value = {
|
|
239
|
+
ok: true,
|
|
240
|
+
fetchedAt: Date.now(),
|
|
241
|
+
status: getCloudflareStatus(),
|
|
242
|
+
zone: zone.ok
|
|
243
|
+
? {
|
|
244
|
+
name: zone.result?.name ?? null,
|
|
245
|
+
plan: zone.result?.plan?.name ?? null,
|
|
246
|
+
status: zone.result?.status ?? null,
|
|
247
|
+
paused: zone.result?.paused === true,
|
|
248
|
+
}
|
|
249
|
+
: null,
|
|
250
|
+
settings: {
|
|
251
|
+
cacheLevel: setting("cache_level")?.value ?? null,
|
|
252
|
+
browserCacheTtl: setting("browser_cache_ttl")?.value ?? null,
|
|
253
|
+
edgeCacheTtl: setting("edge_cache_ttl")?.value ?? null,
|
|
254
|
+
sortQueryString: setting("sort_query_string_for_cache")?.value ?? null,
|
|
255
|
+
alwaysOnline: setting("always_online")?.value ?? null,
|
|
256
|
+
developmentMode: development?.value ?? null,
|
|
257
|
+
// Development mode üç saat sonra kendiliğinden kapanıyor; kalan süre
|
|
258
|
+
// "neden hiçbir şey cache'lenmiyor" sorusunun en sık cevabı.
|
|
259
|
+
developmentModeRemaining: Number(development?.time_remaining) || 0,
|
|
260
|
+
tieredCaching: tiered.ok ? tiered.result?.value ?? null : null,
|
|
261
|
+
cacheReserve: reserve.ok ? reserve.result?.value ?? null : null,
|
|
262
|
+
regionalTieredCache: regional.ok ? regional.result?.value ?? null : null,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
overviewCache = { at: Date.now(), value };
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* ------------------------------------------------------------------ analitik */
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* GraphQL sorgusu. Analitik ayrı bir uçta (`/graphql`) yaşadığı için `call()`
|
|
274
|
+
* kullanılmıyor.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} query
|
|
277
|
+
* @param {Record<string, unknown>} variables
|
|
278
|
+
* @returns {Promise<{ ok: boolean, data?: any, error?: string }>}
|
|
279
|
+
*/
|
|
280
|
+
async function graphql(query, variables) {
|
|
281
|
+
const config = settings();
|
|
282
|
+
if (!config.configured) return { ok: false, error: "Cloudflare is not configured" };
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(GRAPHQL, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: {
|
|
288
|
+
Authorization: `Bearer ${config.token}`,
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
},
|
|
291
|
+
body: JSON.stringify({ query, variables }),
|
|
292
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const payload = await response.json().catch(() => null);
|
|
296
|
+
const error = Array.isArray(payload?.errors) ? payload.errors[0]?.message : null;
|
|
297
|
+
|
|
298
|
+
if (!response.ok || error) {
|
|
299
|
+
return { ok: false, error: error ?? `HTTP ${response.status}` };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { ok: true, data: payload?.data };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { ok: false, error: reason(error) };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* GraphQL analitik penceresinin iki ucu. Yalnızca `datetime_geq` gönderilirse
|
|
310
|
+
* Cloudflare üst sınırı sorguyu işlediği ana koyuyor; Free zone'larda tavan
|
|
311
|
+
* tam 1 gün ve ağ gecikmesi `1d179ms…` gibi bir taşma üretip reddediyor.
|
|
312
|
+
* İki uç aynı `Date.now()`'dan hesaplanınca pencere tam N saat kalır.
|
|
313
|
+
*
|
|
314
|
+
* @param {number} hours
|
|
315
|
+
* @returns {{ since: string, until: string }}
|
|
316
|
+
*/
|
|
317
|
+
function timeWindow(hours) {
|
|
318
|
+
const untilMs = Date.now();
|
|
319
|
+
return {
|
|
320
|
+
since: new Date(untilMs - hours * 3600_000).toISOString(),
|
|
321
|
+
until: new Date(untilMs).toISOString(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Zone'un cache durumu kırılımı: `hit`, `miss`, `dynamic`, `expired`… her biri
|
|
327
|
+
* için istek sayısı ve edge'den giden bayt.
|
|
328
|
+
*
|
|
329
|
+
* `httpRequestsAdaptiveGroups` bilinçli seçildi: `cacheStatus` boyutu eski
|
|
330
|
+
* `httpRequests1hGroups` veri kümesinde yok ve pek çok hesapta hata veriyor.
|
|
331
|
+
* Adaptive küme örnekleme kullanıyor, yani sayılar **tahmindir** — oranlar
|
|
332
|
+
* doğru, mutlak değerler yaklaşık.
|
|
333
|
+
*
|
|
334
|
+
* @param {{ hours?: number }} [options]
|
|
335
|
+
* @returns {Promise<{ ok: boolean, hours: number, sampled: boolean,
|
|
336
|
+
* rows: { status: string, requests: number, bytes: number }[],
|
|
337
|
+
* error?: string }>}
|
|
338
|
+
*/
|
|
339
|
+
export async function fetchCacheAnalytics(options = {}) {
|
|
340
|
+
const config = settings();
|
|
341
|
+
const hours = Math.max(1, Math.min(72, options.hours ?? config.analyticsHours));
|
|
342
|
+
const { since, until } = timeWindow(hours);
|
|
343
|
+
|
|
344
|
+
const query = `
|
|
345
|
+
query CacheStatus($zone: String!, $since: Time!, $until: Time!) {
|
|
346
|
+
viewer {
|
|
347
|
+
zones(filter: { zoneTag: $zone }) {
|
|
348
|
+
httpRequestsAdaptiveGroups(
|
|
349
|
+
filter: { datetime_geq: $since, datetime_leq: $until }
|
|
350
|
+
limit: 50
|
|
351
|
+
orderBy: [count_DESC]
|
|
352
|
+
) {
|
|
353
|
+
count
|
|
354
|
+
sum { edgeResponseBytes }
|
|
355
|
+
dimensions { cacheStatus }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}`;
|
|
360
|
+
|
|
361
|
+
const response = await graphql(query, { zone: config.zoneId, since, until });
|
|
362
|
+
if (!response.ok) return { ok: false, hours, sampled: true, rows: [], error: response.error };
|
|
363
|
+
|
|
364
|
+
const groups = response.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups ?? [];
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
ok: true,
|
|
368
|
+
hours,
|
|
369
|
+
sampled: true,
|
|
370
|
+
rows: groups.map((/** @type {any} */ group) => ({
|
|
371
|
+
status: group.dimensions?.cacheStatus ?? "unknown",
|
|
372
|
+
requests: Number(group.count) || 0,
|
|
373
|
+
bytes: Number(group.sum?.edgeResponseBytes) || 0,
|
|
374
|
+
})),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Bir yolun **kolo bazında** cache kırılımı.
|
|
380
|
+
*
|
|
381
|
+
* Bu, "kaç edge'de bu sayfanın kopyası var" sorusunun API'nin verdiği en yakın
|
|
382
|
+
* cevabı: son `hours` saatte hangi Cloudflare şehri o yolu kaç kez cache'ten
|
|
383
|
+
* (`hit`) ya da origin'den (`miss`, `expired`, `dynamic`) servis etti. Envanter
|
|
384
|
+
* değil, gözlem — hiç istek almamış bir edge listede görünmez, kopyası olsa da.
|
|
385
|
+
*
|
|
386
|
+
* @param {{ path: string, hours?: number }} options
|
|
387
|
+
* @returns {Promise<{ ok: boolean, path: string, hours: number,
|
|
388
|
+
* colos: { colo: string, hits: number, misses: number }[],
|
|
389
|
+
* hits: number, misses: number, error?: string }>}
|
|
390
|
+
*/
|
|
391
|
+
export async function fetchPathEdges({ path, hours }) {
|
|
392
|
+
const config = settings();
|
|
393
|
+
const window = Math.max(1, Math.min(72, hours ?? config.analyticsHours));
|
|
394
|
+
const { since, until } = timeWindow(window);
|
|
395
|
+
|
|
396
|
+
const query = `
|
|
397
|
+
query PathEdges($zone: String!, $since: Time!, $until: Time!, $path: String!) {
|
|
398
|
+
viewer {
|
|
399
|
+
zones(filter: { zoneTag: $zone }) {
|
|
400
|
+
httpRequestsAdaptiveGroups(
|
|
401
|
+
filter: { datetime_geq: $since, datetime_leq: $until, clientRequestPath: $path }
|
|
402
|
+
limit: 500
|
|
403
|
+
orderBy: [count_DESC]
|
|
404
|
+
) {
|
|
405
|
+
count
|
|
406
|
+
dimensions { coloCode cacheStatus }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}`;
|
|
411
|
+
|
|
412
|
+
const response = await graphql(query, {
|
|
413
|
+
zone: config.zoneId,
|
|
414
|
+
since,
|
|
415
|
+
until,
|
|
416
|
+
path,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
return { ok: false, path, hours: window, colos: [], hits: 0, misses: 0, error: response.error };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const groups = response.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups ?? [];
|
|
424
|
+
|
|
425
|
+
/** @type {Map<string, { colo: string, hits: number, misses: number }>} */
|
|
426
|
+
const colos = new Map();
|
|
427
|
+
let hits = 0;
|
|
428
|
+
let misses = 0;
|
|
429
|
+
|
|
430
|
+
for (const group of groups) {
|
|
431
|
+
const colo = group.dimensions?.coloCode ?? "??";
|
|
432
|
+
const status = String(group.dimensions?.cacheStatus ?? "");
|
|
433
|
+
const count = Number(group.count) || 0;
|
|
434
|
+
|
|
435
|
+
const entry = colos.get(colo) ?? { colo, hits: 0, misses: 0 };
|
|
436
|
+
// `hit` dışındaki her şey (miss, expired, dynamic, bypass) origin'e giden
|
|
437
|
+
// ya da hiç cache'lenmeyen istek; panelde tek kolonda toplanıyor.
|
|
438
|
+
if (status === "hit") {
|
|
439
|
+
entry.hits += count;
|
|
440
|
+
hits += count;
|
|
441
|
+
} else {
|
|
442
|
+
entry.misses += count;
|
|
443
|
+
misses += count;
|
|
444
|
+
}
|
|
445
|
+
colos.set(colo, entry);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
path,
|
|
451
|
+
hours: window,
|
|
452
|
+
hits,
|
|
453
|
+
misses,
|
|
454
|
+
colos: [...colos.values()].sort((a, b) => b.hits + b.misses - (a.hits + a.misses)),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* --------------------------------------------------------------------- purge */
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Cloudflare önbelleğini düşürür.
|
|
462
|
+
*
|
|
463
|
+
* Dört hedefleme biçimi de API'nin verdiği hâliyle: `files` (tam URL),
|
|
464
|
+
* `prefixes`, `hosts`, `tags`. Hepsi artık her planda çalışıyor ama istek
|
|
465
|
+
* başına en fazla 100 anahtar kabul ediliyor; uzun listeler burada parçalanır
|
|
466
|
+
* ve **sırayla** gönderilir — paralel göndermek Free planda dakikada beş
|
|
467
|
+
* istekle sınırlı olan hız freni yüzünden yarısı reddedilen bir tur demek.
|
|
468
|
+
*
|
|
469
|
+
* @param {{ everything?: boolean, files?: string[], prefixes?: string[],
|
|
470
|
+
* hosts?: string[], tags?: string[] }} target
|
|
471
|
+
* @returns {Promise<{ ok: boolean, purged: number, batches: number,
|
|
472
|
+
* error?: string }>}
|
|
473
|
+
*/
|
|
474
|
+
export async function purgeCloudflare(target) {
|
|
475
|
+
if (target.everything) {
|
|
476
|
+
const response = await call("/purge_cache", {
|
|
477
|
+
method: "POST",
|
|
478
|
+
body: { purge_everything: true },
|
|
479
|
+
});
|
|
480
|
+
return { ok: response.ok, purged: 0, batches: 1, error: response.error };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** @type {[string, string[]][]} */
|
|
484
|
+
const kinds = [
|
|
485
|
+
["files", target.files ?? []],
|
|
486
|
+
["prefixes", target.prefixes ?? []],
|
|
487
|
+
["hosts", target.hosts ?? []],
|
|
488
|
+
["tags", target.tags ?? []],
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
let purged = 0;
|
|
492
|
+
let batches = 0;
|
|
493
|
+
|
|
494
|
+
for (const [kind, values] of kinds) {
|
|
495
|
+
const unique = [...new Set(values.filter((value) => typeof value === "string" && value))];
|
|
496
|
+
|
|
497
|
+
for (let index = 0; index < unique.length; index += PURGE_BATCH) {
|
|
498
|
+
const slice = unique.slice(index, index + PURGE_BATCH);
|
|
499
|
+
const response = await call("/purge_cache", {
|
|
500
|
+
method: "POST",
|
|
501
|
+
body: { [kind]: slice },
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
batches += 1;
|
|
505
|
+
if (!response.ok) return { ok: false, purged, batches, error: response.error };
|
|
506
|
+
purged += slice.length;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!batches) return { ok: false, purged: 0, batches: 0, error: "nothing to purge" };
|
|
511
|
+
return { ok: true, purged, batches };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Yolları tam URL'e çevirir. Cloudflare `files` purge'ü şemayı da içeren tam
|
|
516
|
+
* URL istiyor; panel elinde yol (`/haber/abc`) tutuyor.
|
|
517
|
+
*
|
|
518
|
+
* @param {string[]} paths
|
|
519
|
+
* @param {string} [fallbackOrigin] `cloudflare.hostname` verilmediğinde
|
|
520
|
+
* kullanılacak kök (panelin açıldığı origin).
|
|
521
|
+
* @returns {string[]}
|
|
522
|
+
*/
|
|
523
|
+
export function toCloudflareUrls(paths, fallbackOrigin) {
|
|
524
|
+
// Yapılandırılmış site adı önce gelir: panel bir iç adresten (ör.
|
|
525
|
+
// `http://10.0.0.4:3000`) açılmış olabilir ve o adresin Cloudflare'de
|
|
526
|
+
// karşılığı yok.
|
|
527
|
+
const hostname = settings().hostname;
|
|
528
|
+
const base = hostname ? `https://${hostname}` : (fallbackOrigin ?? null);
|
|
529
|
+
if (!base) return [];
|
|
530
|
+
|
|
531
|
+
return paths
|
|
532
|
+
.map((path) => {
|
|
533
|
+
try {
|
|
534
|
+
return new URL(path, base).href;
|
|
535
|
+
} catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
.filter((url) => url !== null);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ------------------------------------------------------------------- ayarlar */
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Cache ile ilgili bir zone ayarını değiştirir.
|
|
546
|
+
*
|
|
547
|
+
* Yalnızca beyaz listedeki ayarlar: panelin işi cache, ve bir teşhis
|
|
548
|
+
* arayüzünden zone'un tamamını yeniden yapılandırılabilir yapmak gereksiz bir
|
|
549
|
+
* yüzey.
|
|
550
|
+
*
|
|
551
|
+
* @param {"development_mode" | "cache_level" | "browser_cache_ttl" |
|
|
552
|
+
* "always_online" | "sort_query_string_for_cache"} id
|
|
553
|
+
* @param {string | number} value
|
|
554
|
+
* @returns {Promise<{ ok: boolean, value?: unknown, error?: string }>}
|
|
555
|
+
*/
|
|
556
|
+
export async function setCloudflareSetting(id, value) {
|
|
557
|
+
const allowed = new Set([
|
|
558
|
+
"development_mode",
|
|
559
|
+
"cache_level",
|
|
560
|
+
"browser_cache_ttl",
|
|
561
|
+
"always_online",
|
|
562
|
+
"sort_query_string_for_cache",
|
|
563
|
+
]);
|
|
564
|
+
|
|
565
|
+
if (!allowed.has(id)) return { ok: false, error: `setting not allowed: ${id}` };
|
|
566
|
+
|
|
567
|
+
const response = await call(`/settings/${id}`, { method: "PATCH", body: { value } });
|
|
568
|
+
// Ayar değiştiyse önbelleğe alınmış genel bakış eskidi.
|
|
569
|
+
overviewCache = null;
|
|
570
|
+
|
|
571
|
+
return { ok: response.ok, value: response.result?.value, error: response.error };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Tiered Cache ya da Cache Reserve'ü açar/kapatır. İkisi de ayrı uçlarda ve
|
|
576
|
+
* planla sınırlı; kapalı bir planda Cloudflare açıklayıcı bir hata döner ve
|
|
577
|
+
* panel onu gösterir.
|
|
578
|
+
*
|
|
579
|
+
* @param {"tiered_caching" | "cache_reserve" | "regional_tiered_cache"} feature
|
|
580
|
+
* @param {"on" | "off"} value
|
|
581
|
+
* @returns {Promise<{ ok: boolean, error?: string }>}
|
|
582
|
+
*/
|
|
583
|
+
export async function setCloudflareFeature(feature, value) {
|
|
584
|
+
const path =
|
|
585
|
+
feature === "tiered_caching"
|
|
586
|
+
? "/argo/tiered_caching"
|
|
587
|
+
: feature === "cache_reserve"
|
|
588
|
+
? "/cache/cache_reserve"
|
|
589
|
+
: "/cache/regional_tiered_cache";
|
|
590
|
+
|
|
591
|
+
const response = await call(path, { method: "PATCH", body: { value } });
|
|
592
|
+
overviewCache = null;
|
|
593
|
+
|
|
594
|
+
return { ok: response.ok, error: response.error };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Cache Reserve'ü (R2'deki kalıcı kopya) boşaltır. Purge'den ayrı bir işlem:
|
|
599
|
+
* `purge_everything` edge önbelleklerini düşürüyor, Cache Reserve'deki kopya
|
|
600
|
+
* kalıyor.
|
|
601
|
+
*
|
|
602
|
+
* @returns {Promise<{ ok: boolean, error?: string }>}
|
|
603
|
+
*/
|
|
604
|
+
export async function clearCloudflareCacheReserve() {
|
|
605
|
+
const response = await call("/cache/cache_reserve_clear", { method: "POST" });
|
|
606
|
+
return { ok: response.ok, error: response.error };
|
|
607
|
+
}
|