@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.
- package/CHANGELOG.md +12 -0
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/cli/cloudflare-api.js +267 -0
- package/src/cli/router-module.js +45 -568
- package/src/cli/wrangler-toml.js +324 -0
- package/src/index.js +3 -1
- package/src/node/port-reclaim.js +224 -0
- package/src/node/start-command.js +2 -128
- package/src/runtime/handler/provider-call.js +8 -2
- package/src/runtime/handler/route-debug.js +104 -0
- package/src/runtime/handler/runtime-policy.js +161 -0
- package/src/runtime/handler.js +43 -236
- package/src/shared/timeout-signal.js +23 -0
package/src/cli/router-module.js
CHANGED
|
@@ -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;
|