@khanglvm/llm-router 1.0.8 → 1.0.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.
@@ -36,6 +36,32 @@ import {
36
36
  sanitizeConfigForDisplay,
37
37
  validateRuntimeConfig
38
38
  } from "../runtime/config.js";
39
+ import {
40
+ CLOUDFLARE_ACCOUNT_ID_ENV_NAME,
41
+ CLOUDFLARE_API_TOKEN_ENV_NAME,
42
+ buildCloudflareApiTokenSetupGuide,
43
+ buildCloudflareApiTokenTroubleshooting,
44
+ cloudflareListZones,
45
+ evaluateCloudflareMembershipsResult,
46
+ evaluateCloudflareTokenVerifyResult,
47
+ extractCloudflareMembershipAccounts,
48
+ preflightCloudflareApiToken,
49
+ resolveCloudflareApiTokenFromEnv,
50
+ validateCloudflareApiTokenInput
51
+ } from "./cloudflare-api.js";
52
+ import {
53
+ applyWranglerDeployTargetToToml,
54
+ buildCloudflareDnsManualGuide,
55
+ buildDefaultWranglerTomlForDeploy,
56
+ extractHostnameFromRoutePattern,
57
+ hasNoDeployTargets,
58
+ hasWranglerDeployTargetConfigured,
59
+ inferZoneNameFromHostname,
60
+ isHostnameUnderZone,
61
+ normalizeWranglerRoutePattern,
62
+ parseTomlStringField,
63
+ suggestZoneNameForHostname
64
+ } from "./wrangler-toml.js";
39
65
 
40
66
  const EXIT_SUCCESS = 0;
41
67
  const EXIT_FAILURE = 1;
@@ -48,17 +74,6 @@ const WEAK_MASTER_KEY_PATTERN = /(password|changeme|default|secret|token|admin|q
48
74
  export const CLOUDFLARE_FREE_SECRET_SIZE_LIMIT_BYTES = 5 * 1024;
49
75
  const CLOUDFLARE_FREE_TIER_PATTERN = /\bfree\b/i;
50
76
  const CLOUDFLARE_PAID_TIER_PATTERN = /\b(pro|business|enterprise|paid|unbound)\b/i;
51
- const CLOUDFLARE_API_TOKEN_ENV_NAME = "CLOUDFLARE_API_TOKEN";
52
- const CLOUDFLARE_API_TOKEN_ALT_ENV_NAME = "CF_API_TOKEN";
53
- const CLOUDFLARE_ACCOUNT_ID_ENV_NAME = "CLOUDFLARE_ACCOUNT_ID";
54
- const CLOUDFLARE_API_TOKEN_PRESET_NAME = "Edit Cloudflare Workers";
55
- const CLOUDFLARE_API_TOKEN_DASHBOARD_URL = "https://dash.cloudflare.com/profile/api-tokens";
56
- const CLOUDFLARE_API_TOKEN_GUIDE_URL = "https://developers.cloudflare.com/fundamentals/api/get-started/create-token/";
57
- const CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
58
- const CLOUDFLARE_VERIFY_TOKEN_URL = `${CLOUDFLARE_API_BASE_URL}/user/tokens/verify`;
59
- const CLOUDFLARE_MEMBERSHIPS_URL = `${CLOUDFLARE_API_BASE_URL}/memberships`;
60
- const CLOUDFLARE_ZONES_URL = `${CLOUDFLARE_API_BASE_URL}/zones`;
61
- const CLOUDFLARE_API_PREFLIGHT_TIMEOUT_MS = 10_000;
62
77
  const MODEL_ALIAS_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
63
78
  const MODEL_ROUTING_STRATEGY_OPTIONS = [
64
79
  {
@@ -117,6 +132,25 @@ const RATE_LIMIT_WINDOW_UNIT_ALIASES = new Map([
117
132
  ["months", "month"]
118
133
  ]);
119
134
 
135
+ export {
136
+ applyWranglerDeployTargetToToml,
137
+ buildCloudflareDnsManualGuide,
138
+ buildCloudflareApiTokenSetupGuide,
139
+ buildDefaultWranglerTomlForDeploy,
140
+ evaluateCloudflareMembershipsResult,
141
+ evaluateCloudflareTokenVerifyResult,
142
+ extractHostnameFromRoutePattern,
143
+ extractCloudflareMembershipAccounts,
144
+ hasNoDeployTargets,
145
+ hasWranglerDeployTargetConfigured,
146
+ inferZoneNameFromHostname,
147
+ isHostnameUnderZone,
148
+ normalizeWranglerRoutePattern,
149
+ resolveCloudflareApiTokenFromEnv,
150
+ suggestZoneNameForHostname,
151
+ validateCloudflareApiTokenInput
152
+ };
153
+
120
154
  function canPrompt() {
121
155
  return Boolean(process.stdout.isTTY && process.stdin.isTTY);
122
156
  }
@@ -1232,228 +1266,6 @@ async function runWranglerAsync(args, { cwd, input, envOverrides } = {}) {
1232
1266
  return runCommandAsync(npxCmd, ["wrangler", ...args], { cwd, input, envOverrides });
1233
1267
  }
1234
1268
 
1235
- export function resolveCloudflareApiTokenFromEnv(env = process.env) {
1236
- const primary = String(env?.[CLOUDFLARE_API_TOKEN_ENV_NAME] || "").trim();
1237
- if (primary) {
1238
- return {
1239
- token: primary,
1240
- source: CLOUDFLARE_API_TOKEN_ENV_NAME
1241
- };
1242
- }
1243
-
1244
- const fallback = String(env?.[CLOUDFLARE_API_TOKEN_ALT_ENV_NAME] || "").trim();
1245
- if (fallback) {
1246
- return {
1247
- token: fallback,
1248
- source: CLOUDFLARE_API_TOKEN_ALT_ENV_NAME
1249
- };
1250
- }
1251
-
1252
- return {
1253
- token: "",
1254
- source: "none"
1255
- };
1256
- }
1257
-
1258
- export function buildCloudflareApiTokenSetupGuide() {
1259
- return [
1260
- `Cloudflare deploy requires ${CLOUDFLARE_API_TOKEN_ENV_NAME}.`,
1261
- `Create a User Profile API token in dashboard: ${CLOUDFLARE_API_TOKEN_DASHBOARD_URL}`,
1262
- "Do not use Account API Tokens for this deploy flow.",
1263
- `Token docs: ${CLOUDFLARE_API_TOKEN_GUIDE_URL}`,
1264
- `Recommended preset: ${CLOUDFLARE_API_TOKEN_PRESET_NAME}.`,
1265
- `Then set ${CLOUDFLARE_API_TOKEN_ENV_NAME} in your shell/CI environment.`
1266
- ].join("\n");
1267
- }
1268
-
1269
- export function validateCloudflareApiTokenInput(value) {
1270
- const candidate = String(value || "").trim();
1271
- if (!candidate) return `${CLOUDFLARE_API_TOKEN_ENV_NAME} is required for deploy.`;
1272
- return undefined;
1273
- }
1274
-
1275
- function buildCloudflareApiTokenTroubleshooting(preflightMessage = "") {
1276
- return [
1277
- preflightMessage,
1278
- "Required token capabilities for wrangler deploy:",
1279
- "- User details: Read",
1280
- "- User memberships: Read",
1281
- `- Account preset/template: ${CLOUDFLARE_API_TOKEN_PRESET_NAME}`,
1282
- `Verify token manually: curl \"${CLOUDFLARE_VERIFY_TOKEN_URL}\" -H \"Authorization: Bearer $${CLOUDFLARE_API_TOKEN_ENV_NAME}\"`,
1283
- buildCloudflareApiTokenSetupGuide()
1284
- ].filter(Boolean).join("\n");
1285
- }
1286
-
1287
- function normalizeCloudflareMembershipAccount(entry) {
1288
- if (!entry || typeof entry !== "object") return null;
1289
- const accountObj = entry.account && typeof entry.account === "object" ? entry.account : {};
1290
- const accountId = String(
1291
- accountObj.id
1292
- || entry.account_id
1293
- || entry.accountId
1294
- || entry.id
1295
- || ""
1296
- ).trim();
1297
- if (!accountId) return null;
1298
-
1299
- const accountName = String(
1300
- accountObj.name
1301
- || entry.account_name
1302
- || entry.accountName
1303
- || entry.name
1304
- || `Account ${accountId.slice(0, 8)}`
1305
- ).trim();
1306
-
1307
- return {
1308
- accountId,
1309
- accountName: accountName || `Account ${accountId.slice(0, 8)}`
1310
- };
1311
- }
1312
-
1313
- export function extractCloudflareMembershipAccounts(payload) {
1314
- const list = Array.isArray(payload?.result) ? payload.result : [];
1315
- const map = new Map();
1316
- for (const entry of list) {
1317
- const normalized = normalizeCloudflareMembershipAccount(entry);
1318
- if (!normalized) continue;
1319
- if (!map.has(normalized.accountId)) {
1320
- map.set(normalized.accountId, normalized);
1321
- }
1322
- }
1323
- return Array.from(map.values());
1324
- }
1325
-
1326
- function cloudflareErrorFromPayload(payload, fallback) {
1327
- const base = String(fallback || "Unknown Cloudflare API error");
1328
- if (!payload || typeof payload !== "object") return base;
1329
-
1330
- const errors = Array.isArray(payload.errors) ? payload.errors : [];
1331
- const first = errors.find((entry) => entry && typeof entry === "object");
1332
- if (!first) return base;
1333
-
1334
- const code = Number.isFinite(first.code) ? `code ${first.code}` : "";
1335
- const message = String(first.message || first.error || "").trim();
1336
- if (code && message) return `${message} (${code})`;
1337
- if (message) return message;
1338
- if (code) return code;
1339
- return base;
1340
- }
1341
-
1342
- export function evaluateCloudflareTokenVerifyResult(payload) {
1343
- const status = String(payload?.result?.status || "").toLowerCase();
1344
- const active = payload?.success === true && status === "active";
1345
- if (active) {
1346
- return { ok: true, message: "Token is active." };
1347
- }
1348
- return {
1349
- ok: false,
1350
- message: cloudflareErrorFromPayload(
1351
- payload,
1352
- "Token verification failed. Ensure token is valid and active."
1353
- )
1354
- };
1355
- }
1356
-
1357
- export function evaluateCloudflareMembershipsResult(payload) {
1358
- if (payload?.success !== true || !Array.isArray(payload?.result)) {
1359
- return {
1360
- ok: false,
1361
- message: cloudflareErrorFromPayload(
1362
- payload,
1363
- "Could not list Cloudflare memberships for this token."
1364
- )
1365
- };
1366
- }
1367
-
1368
- if (payload.result.length === 0) {
1369
- return {
1370
- ok: false,
1371
- message: "Token can authenticate but has no accessible memberships."
1372
- };
1373
- }
1374
-
1375
- const accounts = extractCloudflareMembershipAccounts(payload);
1376
- return {
1377
- ok: true,
1378
- message: `Token has access to ${payload.result.length} membership(s).`,
1379
- count: payload.result.length,
1380
- accounts
1381
- };
1382
- }
1383
-
1384
- async function cloudflareApiGetJson(url, token) {
1385
- try {
1386
- const response = await fetch(url, {
1387
- method: "GET",
1388
- headers: {
1389
- Authorization: `Bearer ${token}`
1390
- },
1391
- signal: typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
1392
- ? AbortSignal.timeout(CLOUDFLARE_API_PREFLIGHT_TIMEOUT_MS)
1393
- : undefined
1394
- });
1395
- const rawText = await response.text();
1396
- const payload = parseJsonSafely(rawText) || {};
1397
- return {
1398
- ok: response.ok,
1399
- status: response.status,
1400
- payload
1401
- };
1402
- } catch (error) {
1403
- return {
1404
- ok: false,
1405
- status: 0,
1406
- payload: null,
1407
- error: error instanceof Error ? error.message : String(error)
1408
- };
1409
- }
1410
- }
1411
-
1412
- async function preflightCloudflareApiToken(token) {
1413
- const verified = await cloudflareApiGetJson(CLOUDFLARE_VERIFY_TOKEN_URL, token);
1414
- if (verified.status === 0) {
1415
- return {
1416
- ok: false,
1417
- stage: "verify",
1418
- message: `Cloudflare token preflight failed while verifying token: ${verified.error || "network error"}`
1419
- };
1420
- }
1421
-
1422
- const verifyEval = evaluateCloudflareTokenVerifyResult(verified.payload);
1423
- if (!verified.ok || !verifyEval.ok) {
1424
- return {
1425
- ok: false,
1426
- stage: "verify",
1427
- message: `Cloudflare token verification failed: ${verifyEval.message}`
1428
- };
1429
- }
1430
-
1431
- const memberships = await cloudflareApiGetJson(CLOUDFLARE_MEMBERSHIPS_URL, token);
1432
- if (memberships.status === 0) {
1433
- return {
1434
- ok: false,
1435
- stage: "memberships",
1436
- message: `Cloudflare token preflight failed while checking memberships: ${memberships.error || "network error"}`
1437
- };
1438
- }
1439
-
1440
- const membershipEval = evaluateCloudflareMembershipsResult(memberships.payload);
1441
- if (!memberships.ok || !membershipEval.ok) {
1442
- return {
1443
- ok: false,
1444
- stage: "memberships",
1445
- message: `Cloudflare memberships check failed: ${membershipEval.message}`
1446
- };
1447
- }
1448
-
1449
- return {
1450
- ok: true,
1451
- stage: "ready",
1452
- message: membershipEval.message,
1453
- memberships: membershipEval.accounts || []
1454
- };
1455
- }
1456
-
1457
1269
  function buildWranglerCloudflareEnv({
1458
1270
  apiToken,
1459
1271
  accountId
@@ -1470,255 +1282,11 @@ function formatCloudflareAccountOptions(accounts = []) {
1470
1282
  return (accounts || []).map((entry) => `\`${entry.accountName}\`: \`${entry.accountId}\``);
1471
1283
  }
1472
1284
 
1473
- export function hasNoDeployTargets(outputText = "") {
1474
- return /no deploy targets/i.test(String(outputText || ""));
1475
- }
1476
-
1477
1285
  function parseOptionalBoolean(value) {
1478
1286
  if (value === undefined || value === null || value === "") return undefined;
1479
1287
  return toBoolean(value, false);
1480
1288
  }
1481
1289
 
1482
- function parseTomlStringField(text, key) {
1483
- const pattern = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']+)["']\\s*$`, "m");
1484
- const match = String(text || "").match(pattern);
1485
- return match?.[1] ? String(match[1]).trim() : "";
1486
- }
1487
-
1488
- function topLevelTomlLineInfo(text = "") {
1489
- const lines = String(text || "").split(/\r?\n/g);
1490
- const info = [];
1491
- let currentSection = "";
1492
-
1493
- for (let index = 0; index < lines.length; index += 1) {
1494
- const line = lines[index];
1495
- const trimmed = line.trim();
1496
- if (/^\s*\[.*\]\s*$/.test(line)) {
1497
- currentSection = trimmed;
1498
- }
1499
- info.push({
1500
- index,
1501
- line,
1502
- trimmed,
1503
- section: currentSection
1504
- });
1505
- }
1506
-
1507
- return info;
1508
- }
1509
-
1510
- export function hasWranglerDeployTargetConfigured(tomlText = "") {
1511
- const info = topLevelTomlLineInfo(tomlText);
1512
-
1513
- const hasTopLevelWorkersDev = info.some((entry) =>
1514
- entry.section === "" && /^\s*workers_dev\s*=\s*true\s*$/i.test(entry.line)
1515
- );
1516
- if (hasTopLevelWorkersDev) return true;
1517
-
1518
- const hasTopLevelRoute = info.some((entry) =>
1519
- entry.section === "" && /^\s*route\s*=\s*["'][^"']+["']\s*$/i.test(entry.line)
1520
- );
1521
- if (hasTopLevelRoute) return true;
1522
-
1523
- const hasTopLevelRoutes = info.some((entry) =>
1524
- entry.section === "" && /^\s*routes\s*=\s*\[/i.test(entry.line)
1525
- );
1526
- if (hasTopLevelRoutes) return true;
1527
-
1528
- return false;
1529
- }
1530
-
1531
- function stripNonTopLevelRouteDeclarations(text = "") {
1532
- const lines = String(text || "").split(/\r?\n/g);
1533
- const output = [];
1534
- let currentSection = "";
1535
- let skippingRoutesArray = false;
1536
-
1537
- for (const line of lines) {
1538
- const trimmed = line.trim();
1539
-
1540
- if (/^\s*\[.*\]\s*$/.test(line)) {
1541
- currentSection = trimmed;
1542
- skippingRoutesArray = false;
1543
- output.push(line);
1544
- continue;
1545
- }
1546
-
1547
- if (currentSection && /^\s*route\s*=/.test(line)) {
1548
- continue;
1549
- }
1550
-
1551
- if (currentSection && /^\s*routes\s*=\s*\[/.test(line)) {
1552
- skippingRoutesArray = true;
1553
- if (line.includes("]")) {
1554
- skippingRoutesArray = false;
1555
- }
1556
- continue;
1557
- }
1558
-
1559
- if (skippingRoutesArray) {
1560
- if (trimmed.includes("]")) {
1561
- skippingRoutesArray = false;
1562
- }
1563
- continue;
1564
- }
1565
-
1566
- output.push(line);
1567
- }
1568
-
1569
- return output.join("\n");
1570
- }
1571
-
1572
- function insertTopLevelBlockBeforeFirstSection(text = "", block = "") {
1573
- const source = String(text || "");
1574
- const blockText = String(block || "").trim();
1575
- if (!blockText) return source;
1576
-
1577
- const lines = source.split(/\r?\n/g);
1578
- const firstSectionIndex = lines.findIndex((line) => /^\s*\[.*\]\s*$/.test(line));
1579
- if (firstSectionIndex < 0) {
1580
- const prefix = source.trimEnd();
1581
- return `${prefix}${prefix ? "\n" : ""}${blockText}\n`;
1582
- }
1583
-
1584
- const before = lines.slice(0, firstSectionIndex).join("\n").trimEnd();
1585
- const after = lines.slice(firstSectionIndex).join("\n").trimStart();
1586
- return `${before}${before ? "\n" : ""}${blockText}\n\n${after}\n`;
1587
- }
1588
-
1589
- function upsertTomlBooleanField(text, key, value) {
1590
- const normalized = String(text || "");
1591
- const replacement = `${key} = ${value ? "true" : "false"}`;
1592
- if (new RegExp(`^\\s*${key}\\s*=`, "m").test(normalized)) {
1593
- return normalized.replace(new RegExp(`^\\s*${key}\\s*=.*$`, "m"), replacement);
1594
- }
1595
- return `${normalized.trimEnd()}\n${replacement}\n`;
1596
- }
1597
-
1598
- function stripTopLevelRouteDeclarations(text = "") {
1599
- const lines = String(text || "").split(/\r?\n/g);
1600
- const output = [];
1601
- let currentSection = "";
1602
- let skippingRoutesArray = false;
1603
-
1604
- for (const line of lines) {
1605
- const trimmed = line.trim();
1606
-
1607
- if (/^\s*\[.*\]\s*$/.test(line)) {
1608
- currentSection = trimmed;
1609
- skippingRoutesArray = false;
1610
- output.push(line);
1611
- continue;
1612
- }
1613
-
1614
- if (!currentSection && /^\s*route\s*=/.test(line)) {
1615
- continue;
1616
- }
1617
-
1618
- if (!currentSection && /^\s*routes\s*=\s*\[/.test(line)) {
1619
- skippingRoutesArray = true;
1620
- if (line.includes("]")) {
1621
- skippingRoutesArray = false;
1622
- }
1623
- continue;
1624
- }
1625
-
1626
- if (skippingRoutesArray) {
1627
- if (trimmed.includes("]")) {
1628
- skippingRoutesArray = false;
1629
- }
1630
- continue;
1631
- }
1632
-
1633
- output.push(line);
1634
- }
1635
-
1636
- return output.join("\n");
1637
- }
1638
-
1639
- export function normalizeWranglerRoutePattern(value) {
1640
- const raw = String(value || "").trim();
1641
- if (!raw) return "";
1642
-
1643
- let candidate = raw;
1644
- if (/^https?:\/\//i.test(candidate)) {
1645
- try {
1646
- const parsed = new URL(candidate);
1647
- candidate = `${parsed.hostname}${parsed.pathname || "/"}`;
1648
- } catch {
1649
- return "";
1650
- }
1651
- }
1652
-
1653
- if (candidate.startsWith("/")) return "";
1654
- if (!candidate.includes("*")) {
1655
- if (candidate.endsWith("/")) candidate = `${candidate}*`;
1656
- else if (!candidate.includes("/")) candidate = `${candidate}/*`;
1657
- }
1658
-
1659
- return candidate;
1660
- }
1661
-
1662
- export function buildDefaultWranglerTomlForDeploy({
1663
- name = "llm-router-route",
1664
- main = "src/index.js",
1665
- compatibilityDate = "2024-01-01",
1666
- useWorkersDev = false,
1667
- routePattern = "",
1668
- zoneName = ""
1669
- } = {}) {
1670
- const lines = [
1671
- `name = "${String(name || "llm-router-route")}"`,
1672
- `main = "${String(main || "src/index.js")}"`,
1673
- `compatibility_date = "${String(compatibilityDate || "2024-01-01")}"`,
1674
- `workers_dev = ${useWorkersDev ? "true" : "false"}`
1675
- ];
1676
-
1677
- const normalizedPattern = normalizeWranglerRoutePattern(routePattern);
1678
- const normalizedZone = String(zoneName || "").trim();
1679
- if (!useWorkersDev && normalizedPattern && normalizedZone) {
1680
- lines.push("routes = [");
1681
- lines.push(` { pattern = "${normalizedPattern}", zone_name = "${normalizedZone}" }`);
1682
- lines.push("]");
1683
- }
1684
-
1685
- lines.push("preview_urls = false");
1686
- lines.push("");
1687
- lines.push("[vars]");
1688
- lines.push('ENVIRONMENT = "production"');
1689
- lines.push("");
1690
- return `${lines.join("\n")}`;
1691
- }
1692
-
1693
- export function applyWranglerDeployTargetToToml(existingToml, {
1694
- useWorkersDev = false,
1695
- routePattern = "",
1696
- zoneName = "",
1697
- replaceExistingTarget = false
1698
- } = {}) {
1699
- let next = String(existingToml || "");
1700
- next = stripNonTopLevelRouteDeclarations(next);
1701
- if (replaceExistingTarget) {
1702
- next = stripTopLevelRouteDeclarations(next);
1703
- }
1704
- next = upsertTomlBooleanField(next, "workers_dev", useWorkersDev);
1705
-
1706
- if (!useWorkersDev) {
1707
- const normalizedPattern = normalizeWranglerRoutePattern(routePattern);
1708
- const normalizedZone = String(zoneName || "").trim();
1709
- if (normalizedPattern && normalizedZone && (replaceExistingTarget || !hasWranglerDeployTargetConfigured(next))) {
1710
- const routeBlock = `routes = [\n { pattern = "${normalizedPattern}", zone_name = "${normalizedZone}" }\n]`;
1711
- next = insertTopLevelBlockBeforeFirstSection(next, routeBlock);
1712
- }
1713
- }
1714
-
1715
- if (!/^\s*preview_urls\s*=/mi.test(next)) {
1716
- next = `${next.trimEnd()}\npreview_urls = false\n`;
1717
- }
1718
-
1719
- return `${next.trimEnd()}\n`;
1720
- }
1721
-
1722
1290
  async function createTemporaryWranglerConfigFile(projectDir, tomlText) {
1723
1291
  await fsPromises.mkdir(projectDir, { recursive: true });
1724
1292
  const suffix = `${Date.now()}-${randomBytes(4).toString("hex")}`;
@@ -1925,97 +1493,6 @@ async function prepareWranglerDeployConfig(context, {
1925
1493
  };
1926
1494
  }
1927
1495
 
1928
-
1929
- function normalizeHostname(value) {
1930
- return String(value || "")
1931
- .trim()
1932
- .toLowerCase()
1933
- .replace(/^https?:\/\//, "")
1934
- .replace(/\/.*$/, "")
1935
- .replace(/:\d+$/, "")
1936
- .replace(/\.$/, "");
1937
- }
1938
-
1939
- export function extractHostnameFromRoutePattern(value) {
1940
- const route = String(value || "").trim();
1941
- if (!route) return "";
1942
-
1943
- if (/^https?:\/\//i.test(route)) {
1944
- try {
1945
- return normalizeHostname(new URL(route).hostname);
1946
- } catch {
1947
- return "";
1948
- }
1949
- }
1950
-
1951
- const left = route.split("/")[0] || "";
1952
- return normalizeHostname(left.replace(/\*+$/g, ""));
1953
- }
1954
-
1955
- export function inferZoneNameFromHostname(hostname) {
1956
- const host = normalizeHostname(hostname);
1957
- if (!host || !host.includes(".")) return "";
1958
- const labels = host.split(".").filter(Boolean);
1959
- if (labels.length <= 2) return host;
1960
- return labels.slice(-2).join(".");
1961
- }
1962
-
1963
- export function isHostnameUnderZone(hostname, zoneName) {
1964
- const host = normalizeHostname(hostname);
1965
- const zone = normalizeHostname(zoneName);
1966
- if (!host || !zone) return false;
1967
- return host === zone || host.endsWith(`.${zone}`);
1968
- }
1969
-
1970
- export function suggestZoneNameForHostname(hostname, zones = []) {
1971
- const host = normalizeHostname(hostname);
1972
- if (!host) return "";
1973
-
1974
- let best = "";
1975
- for (const zone of zones || []) {
1976
- const candidate = normalizeHostname(zone?.name || zone);
1977
- if (!candidate) continue;
1978
- if (host === candidate || host.endsWith(`.${candidate}`)) {
1979
- if (!best || candidate.length > best.length) {
1980
- best = candidate;
1981
- }
1982
- }
1983
- }
1984
- return best;
1985
- }
1986
-
1987
- export function buildCloudflareDnsManualGuide({
1988
- hostname = "",
1989
- zoneName = "",
1990
- routePattern = ""
1991
- } = {}) {
1992
- const host = normalizeHostname(hostname || extractHostnameFromRoutePattern(routePattern));
1993
- const zone = normalizeHostname(zoneName || inferZoneNameFromHostname(host));
1994
- const subdomain = host && zone && host.endsWith(`.${zone}`)
1995
- ? host.slice(0, -(`.${zone}`).length)
1996
- : "";
1997
- const label = subdomain || "<subdomain>";
1998
-
1999
- return [
2000
- "Custom domain checklist:",
2001
- `- Route target: ${routePattern || `${host || "<host>"}/*`} (zone: ${zone || "<zone>"})`,
2002
- `- DNS: create/update CNAME \`${label}\` -> \`@\` in zone \`${zone || "<zone>"}\``,
2003
- "- Proxy status must be ON (orange cloud / proxied)",
2004
- host ? `- Verify DNS: dig +short ${host} @1.1.1.1` : "- Verify DNS: dig +short <host> @1.1.1.1",
2005
- host ? `- Verify HTTP: curl -I https://${host}/anthropic` : "- Verify HTTP: curl -I https://<host>/anthropic",
2006
- "- Claude base URL must NOT include :8787 for Cloudflare Worker deployments"
2007
- ].join("\n");
2008
- }
2009
-
2010
- async function cloudflareListZones(token, accountId = "") {
2011
- const params = new URLSearchParams({ per_page: "50" });
2012
- if (accountId) params.set("account.id", accountId);
2013
- const result = await cloudflareApiGetJson(`${CLOUDFLARE_ZONES_URL}?${params.toString()}`, token);
2014
- if (!result.ok || !Array.isArray(result.payload?.result)) return [];
2015
- return result.payload.result
2016
- .map((zone) => ({ id: String(zone?.id || "").trim(), name: normalizeHostname(zone?.name || "") }))
2017
- .filter((zone) => zone.id && zone.name);
2018
- }
2019
1496
  function parseJsonSafely(value) {
2020
1497
  const text = String(value || "").trim();
2021
1498
  if (!text) return null;