@utilix-tech/sdk 0.9.0 → 0.11.0
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 +7 -0
- package/README.md +19 -3
- package/dist/{chunk-QJE743LY.js → chunk-GGDDCTFM.js} +224 -1
- package/dist/{chunk-LI5AME5R.js → chunk-OB56VNKJ.js} +85 -1
- package/dist/index.cjs +311 -4
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{network-Cy02-FIO.d.cts → network-DaNoZ5ER.d.cts} +39 -2
- package/dist/{network-Cy02-FIO.d.ts → network-DaNoZ5ER.d.ts} +39 -2
- package/dist/tools/network.cjs +84 -0
- package/dist/tools/network.d.cts +1 -1
- package/dist/tools/network.d.ts +1 -1
- package/dist/tools/network.js +1 -1
- package/dist/tools/units.cjs +223 -0
- package/dist/tools/units.d.cts +1 -1
- package/dist/tools/units.d.ts +1 -1
- package/dist/tools/units.js +1 -1
- package/dist/{units-6lwDYBvX.d.cts → units-BG387sch.d.cts} +67 -2
- package/dist/{units-6lwDYBvX.d.ts → units-BG387sch.d.ts} +67 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
|
|
4
4
|
|
|
5
|
+
## 0.11.0
|
|
6
|
+
- Added IBAN Validator / Formatter (`validateIban`) and Loan / Mortgage Calculator (`calculateLoan`).
|
|
7
|
+
- (npm publish for this version was retried after fixing a CI pin: `npm install -g npm@latest` started pulling npm 12, which requires Node >=22 and broke on this job's pinned Node 20 — now pinned to the npm 11 line.)
|
|
8
|
+
|
|
9
|
+
## 0.10.0
|
|
10
|
+
- Added robots.txt Validator and Credit Card Number Validator.
|
|
11
|
+
|
|
5
12
|
## 0.9.0
|
|
6
13
|
- Added Sitemap.xml Validator.
|
|
7
14
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/sdk
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**533 developer utility functions for Node.js: runs locally, no API key required.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
6
6
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
@@ -242,7 +242,7 @@ getNextRuns("*/5 * * * *", 5); // next 5 run timestamps
|
|
|
242
242
|
### `/units`: Conversions
|
|
243
243
|
|
|
244
244
|
```ts
|
|
245
|
-
import { convertBytes, pxToAll, convert, formatValue } from "@utilix-tech/sdk/units";
|
|
245
|
+
import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan } from "@utilix-tech/sdk/units";
|
|
246
246
|
|
|
247
247
|
// File sizes
|
|
248
248
|
convertBytes(1073741824, "GB"); // 1
|
|
@@ -260,6 +260,18 @@ convert("11111111", 2, 10); // "255"
|
|
|
260
260
|
// Currency / locale formatting
|
|
261
261
|
formatValue(1234567.89, { locale: "en-US", currency: "USD" });
|
|
262
262
|
// "$1,234,567.89"
|
|
263
|
+
|
|
264
|
+
// Credit card Luhn checksum + network detection
|
|
265
|
+
validateCardNumber("4111 1111 1111 1111");
|
|
266
|
+
// { valid: true, network: "Visa", length: 16, formatted: "4111 1111 1111 1111" }
|
|
267
|
+
|
|
268
|
+
// IBAN mod-97 checksum + formatting
|
|
269
|
+
validateIban("DE89 3704 0044 0532 0130 00");
|
|
270
|
+
// { valid: true, countryCode: "DE", checksumValid: true, formatted: "DE89 3704 0044 0532 0130 00", ... }
|
|
271
|
+
|
|
272
|
+
// Loan / mortgage amortization
|
|
273
|
+
calculateLoan(200000, 6.5, 360);
|
|
274
|
+
// { monthlyPayment: 1264.14, totalInterest: 255085.82, termMonths: 360, schedule: [...] }
|
|
263
275
|
```
|
|
264
276
|
|
|
265
277
|
---
|
|
@@ -269,7 +281,7 @@ formatValue(1234567.89, { locale: "en-US", currency: "USD" });
|
|
|
269
281
|
```ts
|
|
270
282
|
import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
|
|
271
283
|
searchStatusCodes, buildGeoUrl, countryCodeToFlag,
|
|
272
|
-
parseHar, validateSitemap } from "@utilix-tech/sdk/network";
|
|
284
|
+
parseHar, validateSitemap, validateRobotsTxt } from "@utilix-tech/sdk/network";
|
|
273
285
|
|
|
274
286
|
getStatusCode(404);
|
|
275
287
|
// { code: 404, text: "Not Found", description: "...", category: "Client Error" }
|
|
@@ -290,6 +302,10 @@ parseHar(har);
|
|
|
290
302
|
// Validate a sitemap.xml (or sitemap index) against the sitemaps.org protocol
|
|
291
303
|
validateSitemap('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://example.com/</loc></url></urlset>');
|
|
292
304
|
// { type: "urlset", urlCount: 1, entries: [...], issues: [...], valid: true }
|
|
305
|
+
|
|
306
|
+
// Validate a robots.txt file: User-agent groups, rules, and syntax mistakes
|
|
307
|
+
validateRobotsTxt("User-agent: *\nDisallow: /admin\nSitemap: https://example.com/sitemap.xml");
|
|
308
|
+
// { groups: [...], sitemaps: [...], issues: [...], valid: true }
|
|
293
309
|
```
|
|
294
310
|
|
|
295
311
|
---
|
|
@@ -16,6 +16,7 @@ __export(units_exports, {
|
|
|
16
16
|
bumpVersion: () => bumpVersion,
|
|
17
17
|
calcAspectRatio: () => calcAspectRatio,
|
|
18
18
|
calcSubnet: () => calcSubnet,
|
|
19
|
+
calculateLoan: () => calculateLoan,
|
|
19
20
|
compareSemver: () => compareSemver,
|
|
20
21
|
convert: () => convert,
|
|
21
22
|
convertBytes: () => convertBytes,
|
|
@@ -60,6 +61,8 @@ __export(units_exports, {
|
|
|
60
61
|
sortVersions: () => sortVersions,
|
|
61
62
|
splitSubnet: () => splitSubnet,
|
|
62
63
|
symbolicToPermissions: () => symbolicToPermissions,
|
|
64
|
+
validateCardNumber: () => validateCardNumber,
|
|
65
|
+
validateIban: () => validateIban,
|
|
63
66
|
viewportToPx: () => viewportToPx
|
|
64
67
|
});
|
|
65
68
|
function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
|
|
@@ -892,5 +895,225 @@ function sortVersions(versions, order = "asc") {
|
|
|
892
895
|
return order === "asc" ? cmp : -cmp;
|
|
893
896
|
});
|
|
894
897
|
}
|
|
898
|
+
var CARD_NETWORK_RULES = [
|
|
899
|
+
{ name: "American Express", lengths: [15], test: (d) => /^3[47]/.test(d) },
|
|
900
|
+
{ name: "Diners Club", lengths: [14], test: (d) => /^3(?:0[0-5]|[68])/.test(d) },
|
|
901
|
+
{
|
|
902
|
+
name: "Discover",
|
|
903
|
+
lengths: [16, 19],
|
|
904
|
+
test: (d) => /^(?:6011|65|64[4-9])/.test(d) || /^622(?:12[6-9]|1[3-9]\d|[2-8]\d{2}|9[01]\d|92[0-5])/.test(d)
|
|
905
|
+
},
|
|
906
|
+
{ name: "JCB", lengths: [16], test: (d) => /^35(?:2[89]|[3-8]\d)/.test(d) },
|
|
907
|
+
{
|
|
908
|
+
name: "Mastercard",
|
|
909
|
+
lengths: [16],
|
|
910
|
+
test: (d) => {
|
|
911
|
+
const two = parseInt(d.slice(0, 2), 10);
|
|
912
|
+
const four = parseInt(d.slice(0, 4), 10);
|
|
913
|
+
return two >= 51 && two <= 55 || four >= 2221 && four <= 2720;
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
{ name: "Maestro", lengths: [12, 13, 14, 15, 16, 17, 18, 19], test: (d) => /^(?:50|5[6-8]|6304|6390|67)/.test(d) },
|
|
917
|
+
{ name: "UnionPay", lengths: [16, 17, 18, 19], test: (d) => /^62/.test(d) },
|
|
918
|
+
{ name: "Visa", lengths: [13, 16, 19], test: (d) => /^4/.test(d) }
|
|
919
|
+
];
|
|
920
|
+
function luhnCheck(digits) {
|
|
921
|
+
let sum = 0;
|
|
922
|
+
let shouldDouble = false;
|
|
923
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
924
|
+
let d = parseInt(digits[i], 10);
|
|
925
|
+
if (shouldDouble) {
|
|
926
|
+
d *= 2;
|
|
927
|
+
if (d > 9) d -= 9;
|
|
928
|
+
}
|
|
929
|
+
sum += d;
|
|
930
|
+
shouldDouble = !shouldDouble;
|
|
931
|
+
}
|
|
932
|
+
return sum % 10 === 0;
|
|
933
|
+
}
|
|
934
|
+
function detectCardNetwork(digits) {
|
|
935
|
+
for (const rule of CARD_NETWORK_RULES) {
|
|
936
|
+
if (rule.lengths.includes(digits.length) && rule.test(digits)) return rule.name;
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
function formatCardNumber(digits, network) {
|
|
941
|
+
if (network === "American Express") {
|
|
942
|
+
return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10)}`.trim();
|
|
943
|
+
}
|
|
944
|
+
return digits.match(/.{1,4}/g)?.join(" ") ?? digits;
|
|
945
|
+
}
|
|
946
|
+
function validateCardNumber(input) {
|
|
947
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
948
|
+
const digits = input.replace(/[\s-]/g, "");
|
|
949
|
+
if (!/^\d+$/.test(digits)) return { error: "Card number must contain only digits, spaces, or hyphens" };
|
|
950
|
+
if (digits.length < 12 || digits.length > 19) {
|
|
951
|
+
return { error: `Card number length (${digits.length}) is outside the valid range of 12-19 digits` };
|
|
952
|
+
}
|
|
953
|
+
const valid = luhnCheck(digits);
|
|
954
|
+
const network = detectCardNetwork(digits);
|
|
955
|
+
const formatted = formatCardNumber(digits, network);
|
|
956
|
+
return { valid, network, length: digits.length, formatted };
|
|
957
|
+
}
|
|
958
|
+
var IBAN_COUNTRY_LENGTHS = {
|
|
959
|
+
AD: 24,
|
|
960
|
+
AE: 23,
|
|
961
|
+
AL: 28,
|
|
962
|
+
AT: 20,
|
|
963
|
+
AZ: 28,
|
|
964
|
+
BA: 20,
|
|
965
|
+
BE: 16,
|
|
966
|
+
BG: 22,
|
|
967
|
+
BH: 22,
|
|
968
|
+
BR: 29,
|
|
969
|
+
BY: 28,
|
|
970
|
+
CH: 21,
|
|
971
|
+
CR: 22,
|
|
972
|
+
CY: 28,
|
|
973
|
+
CZ: 24,
|
|
974
|
+
DE: 22,
|
|
975
|
+
DK: 18,
|
|
976
|
+
DO: 28,
|
|
977
|
+
EE: 20,
|
|
978
|
+
EG: 29,
|
|
979
|
+
ES: 24,
|
|
980
|
+
FI: 18,
|
|
981
|
+
FO: 18,
|
|
982
|
+
FR: 27,
|
|
983
|
+
GB: 22,
|
|
984
|
+
GE: 22,
|
|
985
|
+
GI: 23,
|
|
986
|
+
GL: 18,
|
|
987
|
+
GR: 27,
|
|
988
|
+
GT: 28,
|
|
989
|
+
HR: 21,
|
|
990
|
+
HU: 28,
|
|
991
|
+
IE: 22,
|
|
992
|
+
IL: 23,
|
|
993
|
+
IQ: 23,
|
|
994
|
+
IS: 26,
|
|
995
|
+
IT: 27,
|
|
996
|
+
JO: 30,
|
|
997
|
+
KW: 30,
|
|
998
|
+
KZ: 20,
|
|
999
|
+
LB: 28,
|
|
1000
|
+
LC: 32,
|
|
1001
|
+
LI: 21,
|
|
1002
|
+
LT: 20,
|
|
1003
|
+
LU: 20,
|
|
1004
|
+
LV: 21,
|
|
1005
|
+
LY: 25,
|
|
1006
|
+
MC: 27,
|
|
1007
|
+
MD: 24,
|
|
1008
|
+
ME: 22,
|
|
1009
|
+
MK: 19,
|
|
1010
|
+
MR: 27,
|
|
1011
|
+
MT: 31,
|
|
1012
|
+
MU: 30,
|
|
1013
|
+
NL: 18,
|
|
1014
|
+
NO: 15,
|
|
1015
|
+
PK: 24,
|
|
1016
|
+
PL: 28,
|
|
1017
|
+
PS: 29,
|
|
1018
|
+
PT: 25,
|
|
1019
|
+
QA: 29,
|
|
1020
|
+
RO: 24,
|
|
1021
|
+
RS: 22,
|
|
1022
|
+
SA: 24,
|
|
1023
|
+
SC: 31,
|
|
1024
|
+
SE: 24,
|
|
1025
|
+
SI: 19,
|
|
1026
|
+
SK: 24,
|
|
1027
|
+
SM: 27,
|
|
1028
|
+
ST: 25,
|
|
1029
|
+
SV: 28,
|
|
1030
|
+
TL: 23,
|
|
1031
|
+
TN: 24,
|
|
1032
|
+
TR: 26,
|
|
1033
|
+
UA: 29,
|
|
1034
|
+
VA: 22,
|
|
1035
|
+
VG: 24,
|
|
1036
|
+
XK: 20
|
|
1037
|
+
};
|
|
1038
|
+
function ibanMod97(numeric) {
|
|
1039
|
+
let remainder = 0;
|
|
1040
|
+
for (let i = 0; i < numeric.length; i += 7) {
|
|
1041
|
+
remainder = parseInt(String(remainder) + numeric.substr(i, 7), 10) % 97;
|
|
1042
|
+
}
|
|
1043
|
+
return remainder;
|
|
1044
|
+
}
|
|
1045
|
+
function formatIban(cleaned) {
|
|
1046
|
+
return cleaned.match(/.{1,4}/g)?.join(" ") ?? cleaned;
|
|
1047
|
+
}
|
|
1048
|
+
function validateIban(input) {
|
|
1049
|
+
if (!input || !input.trim()) return { error: "Input is empty" };
|
|
1050
|
+
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
1051
|
+
if (!/^[A-Z0-9]+$/.test(cleaned)) return { error: "IBAN must contain only letters and digits" };
|
|
1052
|
+
if (cleaned.length < 4 || cleaned.length > 34) {
|
|
1053
|
+
return { error: `IBAN length (${cleaned.length}) is outside the valid range of 4-34 characters` };
|
|
1054
|
+
}
|
|
1055
|
+
if (!/^[A-Z]{2}\d{2}/.test(cleaned)) {
|
|
1056
|
+
return { error: "IBAN must start with a 2-letter country code followed by 2 check digits" };
|
|
1057
|
+
}
|
|
1058
|
+
const countryCode = cleaned.slice(0, 2);
|
|
1059
|
+
const checkDigits = cleaned.slice(2, 4);
|
|
1060
|
+
const bban = cleaned.slice(4);
|
|
1061
|
+
const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
|
|
1062
|
+
const numeric = rearranged.split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
|
|
1063
|
+
const checksumValid = ibanMod97(numeric) === 1;
|
|
1064
|
+
const expectedLength = IBAN_COUNTRY_LENGTHS[countryCode] ?? null;
|
|
1065
|
+
const lengthValid = expectedLength === null ? null : cleaned.length === expectedLength;
|
|
1066
|
+
const valid = checksumValid && lengthValid !== false;
|
|
1067
|
+
return {
|
|
1068
|
+
valid,
|
|
1069
|
+
formatted: formatIban(cleaned),
|
|
1070
|
+
countryCode,
|
|
1071
|
+
checkDigits,
|
|
1072
|
+
bban,
|
|
1073
|
+
length: cleaned.length,
|
|
1074
|
+
expectedLength,
|
|
1075
|
+
checksumValid,
|
|
1076
|
+
lengthValid
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function round2(n) {
|
|
1080
|
+
return Math.round(n * 100) / 100;
|
|
1081
|
+
}
|
|
1082
|
+
function calculateLoan(principal, annualRatePercent, termMonths) {
|
|
1083
|
+
if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a positive number" };
|
|
1084
|
+
if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
|
|
1085
|
+
return { error: "Annual interest rate must be zero or a positive number" };
|
|
1086
|
+
}
|
|
1087
|
+
if (!Number.isInteger(termMonths) || termMonths <= 0) {
|
|
1088
|
+
return { error: "Term must be a positive whole number of months" };
|
|
1089
|
+
}
|
|
1090
|
+
const monthlyRate = annualRatePercent / 100 / 12;
|
|
1091
|
+
let monthlyPayment;
|
|
1092
|
+
if (monthlyRate === 0) {
|
|
1093
|
+
monthlyPayment = principal / termMonths;
|
|
1094
|
+
} else {
|
|
1095
|
+
const factor = Math.pow(1 + monthlyRate, termMonths);
|
|
1096
|
+
monthlyPayment = principal * monthlyRate * factor / (factor - 1);
|
|
1097
|
+
}
|
|
1098
|
+
const schedule = [];
|
|
1099
|
+
let balance = round2(principal);
|
|
1100
|
+
let totalPayment = 0;
|
|
1101
|
+
for (let month = 1; month <= termMonths; month++) {
|
|
1102
|
+
const interest = monthlyRate === 0 ? 0 : round2(balance * monthlyRate);
|
|
1103
|
+
let principalPortion = round2(monthlyPayment) - interest;
|
|
1104
|
+
if (month === termMonths || principalPortion > balance) principalPortion = balance;
|
|
1105
|
+
const payment = round2(principalPortion + interest);
|
|
1106
|
+
balance = round2(balance - principalPortion);
|
|
1107
|
+
totalPayment += payment;
|
|
1108
|
+
schedule.push({ month, payment, principal: round2(principalPortion), interest, balance: Math.max(balance, 0) });
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
monthlyPayment: round2(monthlyPayment),
|
|
1112
|
+
totalPayment: round2(totalPayment),
|
|
1113
|
+
totalInterest: round2(totalPayment - principal),
|
|
1114
|
+
termMonths,
|
|
1115
|
+
schedule
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
895
1118
|
|
|
896
|
-
export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_VIEWPORTS, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, compareSemver, convert, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, viewportToPx };
|
|
1119
|
+
export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_VIEWPORTS, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateLoan, compareSemver, convert, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateCardNumber, validateIban, viewportToPx };
|
|
@@ -33,6 +33,7 @@ __export(network_exports, {
|
|
|
33
33
|
validateDomain: () => validateDomain,
|
|
34
34
|
validateHeaderName: () => validateHeaderName,
|
|
35
35
|
validateHeaderValue: () => validateHeaderValue,
|
|
36
|
+
validateRobotsTxt: () => validateRobotsTxt,
|
|
36
37
|
validateSitemap: () => validateSitemap
|
|
37
38
|
});
|
|
38
39
|
var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SOA", "PTR", "SRV", "CAA"];
|
|
@@ -1531,5 +1532,88 @@ function validateSitemap(xml) {
|
|
|
1531
1532
|
const valid = !issues.some((i) => i.severity === "error");
|
|
1532
1533
|
return { type, urlCount: entries.length, entries, issues, valid };
|
|
1533
1534
|
}
|
|
1535
|
+
var KNOWN_ROBOTS_DIRECTIVES = /* @__PURE__ */ new Set(["user-agent", "disallow", "allow", "sitemap", "crawl-delay", "host"]);
|
|
1536
|
+
function validateRobotsTxt(text) {
|
|
1537
|
+
if (!text || !text.trim()) return { error: "Input is empty" };
|
|
1538
|
+
const lines = text.split("\n");
|
|
1539
|
+
const issues = [];
|
|
1540
|
+
const groups = [];
|
|
1541
|
+
const sitemaps = [];
|
|
1542
|
+
let currentGroup = null;
|
|
1543
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1544
|
+
const rawLine = lines[i];
|
|
1545
|
+
const lineNum = i + 1;
|
|
1546
|
+
const commentIdx = rawLine.indexOf("#");
|
|
1547
|
+
const line = (commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine).trim();
|
|
1548
|
+
if (!line) continue;
|
|
1549
|
+
const colonIdx = line.indexOf(":");
|
|
1550
|
+
if (colonIdx === -1) {
|
|
1551
|
+
issues.push({ severity: "error", line: lineNum, message: `Malformed line (missing ':'): "${line}"` });
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
const directive = line.slice(0, colonIdx).trim().toLowerCase();
|
|
1555
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
1556
|
+
if (!KNOWN_ROBOTS_DIRECTIVES.has(directive)) {
|
|
1557
|
+
issues.push({ severity: "warning", line: lineNum, message: `Unknown directive "${directive}"` });
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (directive === "sitemap") {
|
|
1561
|
+
if (!/^https?:\/\//i.test(value)) {
|
|
1562
|
+
issues.push({ severity: "error", line: lineNum, message: `Sitemap URL "${value}" must be absolute (start with http:// or https://)` });
|
|
1563
|
+
}
|
|
1564
|
+
sitemaps.push(value);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
if (directive === "user-agent") {
|
|
1568
|
+
if (!value) {
|
|
1569
|
+
issues.push({ severity: "error", line: lineNum, message: "User-agent directive is missing a value" });
|
|
1570
|
+
}
|
|
1571
|
+
if (currentGroup && currentGroup.rules.length === 0 && currentGroup.crawlDelay === void 0) {
|
|
1572
|
+
currentGroup.userAgents.push(value);
|
|
1573
|
+
} else {
|
|
1574
|
+
currentGroup = { userAgents: [value], rules: [], line: lineNum };
|
|
1575
|
+
groups.push(currentGroup);
|
|
1576
|
+
}
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
if (!currentGroup) {
|
|
1580
|
+
issues.push({ severity: "error", line: lineNum, message: `"${directive}" directive found before any User-agent line` });
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if (directive === "disallow" || directive === "allow") {
|
|
1584
|
+
if (value !== "" && !value.startsWith("/") && !value.startsWith("*")) {
|
|
1585
|
+
issues.push({ severity: "warning", line: lineNum, message: `${directive === "disallow" ? "Disallow" : "Allow"} value "${value}" should start with "/" (or be empty)` });
|
|
1586
|
+
}
|
|
1587
|
+
if (currentGroup.rules.some((r) => r.type === directive && r.value === value)) {
|
|
1588
|
+
issues.push({ severity: "warning", line: lineNum, message: `Duplicate ${directive} rule for "${value}" in this group` });
|
|
1589
|
+
}
|
|
1590
|
+
currentGroup.rules.push({ type: directive, value, line: lineNum });
|
|
1591
|
+
} else if (directive === "crawl-delay") {
|
|
1592
|
+
const n = Number(value);
|
|
1593
|
+
if (Number.isNaN(n) || n < 0) {
|
|
1594
|
+
issues.push({ severity: "error", line: lineNum, message: `Crawl-delay "${value}" must be a non-negative number` });
|
|
1595
|
+
} else {
|
|
1596
|
+
currentGroup.crawlDelay = n;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (groups.length === 0) {
|
|
1601
|
+
issues.push({ severity: "error", line: 1, message: "No User-agent groups found" });
|
|
1602
|
+
}
|
|
1603
|
+
const seenUA = /* @__PURE__ */ new Map();
|
|
1604
|
+
for (const g of groups) {
|
|
1605
|
+
for (const ua of g.userAgents) {
|
|
1606
|
+
const key = ua.toLowerCase();
|
|
1607
|
+
const firstLine = seenUA.get(key);
|
|
1608
|
+
if (firstLine !== void 0) {
|
|
1609
|
+
issues.push({ severity: "warning", line: g.line, message: `Duplicate User-agent "${ua}" (first seen on line ${firstLine})` });
|
|
1610
|
+
} else {
|
|
1611
|
+
seenUA.set(key, g.line);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
const valid = !issues.some((i) => i.severity === "error");
|
|
1616
|
+
return { groups, sitemaps, issues, valid };
|
|
1617
|
+
}
|
|
1534
1618
|
|
|
1535
|
-
export { DNS_RECORD_TYPES, HTTP_HEADERS, STATUS_CODES, buildDnsQueryUrl, buildGeoUrl, buildMapUrl, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseDnsResponse, parseGeoResponse, parseHar, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateSitemap };
|
|
1619
|
+
export { DNS_RECORD_TYPES, HTTP_HEADERS, STATUS_CODES, buildDnsQueryUrl, buildGeoUrl, buildMapUrl, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseDnsResponse, parseGeoResponse, parseHar, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateRobotsTxt, validateSitemap };
|