@utilix-tech/sdk 0.8.0 → 0.10.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 CHANGED
@@ -2,6 +2,12 @@
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.10.0
6
+ - Added robots.txt Validator and Credit Card Number Validator.
7
+
8
+ ## 0.9.0
9
+ - Added Sitemap.xml Validator.
10
+
5
11
  ## 0.8.0
6
12
  - Added ID3 Tag Reader and .env.example Diff Checker.
7
13
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **526 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **531 developer utility functions for Node.js: runs locally, no API key required.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@utilix-tech/sdk?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@utilix-tech/sdk)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@utilix-tech/sdk?style=flat-square)](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 } from "@utilix-tech/sdk/units";
246
246
 
247
247
  // File sizes
248
248
  convertBytes(1073741824, "GB"); // 1
@@ -260,6 +260,10 @@ 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" }
263
267
  ```
264
268
 
265
269
  ---
@@ -269,7 +273,7 @@ formatValue(1234567.89, { locale: "en-US", currency: "USD" });
269
273
  ```ts
270
274
  import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
271
275
  searchStatusCodes, buildGeoUrl, countryCodeToFlag,
272
- parseHar } from "@utilix-tech/sdk/network";
276
+ parseHar, validateSitemap, validateRobotsTxt } from "@utilix-tech/sdk/network";
273
277
 
274
278
  getStatusCode(404);
275
279
  // { code: 404, text: "Not Found", description: "...", category: "Client Error" }
@@ -286,6 +290,14 @@ countryCodeToFlag("US"); // "🇺🇸"
286
290
  const har = await fs.promises.readFile("network.har", "utf-8");
287
291
  parseHar(har);
288
292
  // { version: "1.2", entries: [...], summary: { totalRequests, totalSize, failedRequests, ... } }
293
+
294
+ // Validate a sitemap.xml (or sitemap index) against the sitemaps.org protocol
295
+ validateSitemap('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://example.com/</loc></url></urlset>');
296
+ // { type: "urlset", urlCount: 1, entries: [...], issues: [...], valid: true }
297
+
298
+ // Validate a robots.txt file: User-agent groups, rules, and syntax mistakes
299
+ validateRobotsTxt("User-agent: *\nDisallow: /admin\nSitemap: https://example.com/sitemap.xml");
300
+ // { groups: [...], sitemaps: [...], issues: [...], valid: true }
289
301
  ```
290
302
 
291
303
  ---
@@ -32,7 +32,9 @@ __export(network_exports, {
32
32
  typeNumberToLabel: () => typeNumberToLabel,
33
33
  validateDomain: () => validateDomain,
34
34
  validateHeaderName: () => validateHeaderName,
35
- validateHeaderValue: () => validateHeaderValue
35
+ validateHeaderValue: () => validateHeaderValue,
36
+ validateRobotsTxt: () => validateRobotsTxt,
37
+ validateSitemap: () => validateSitemap
36
38
  });
37
39
  var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SOA", "PTR", "SRV", "CAA"];
38
40
  function buildDnsQueryUrl(domain, type) {
@@ -1440,5 +1442,178 @@ function parseHar(input) {
1440
1442
  summary: { totalRequests, totalSize, totalTime, avgTime, failedRequests, methods, statusCodes }
1441
1443
  };
1442
1444
  }
1445
+ var VALID_CHANGEFREQ = /* @__PURE__ */ new Set(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]);
1446
+ function sitemapLineNumber(xml, index) {
1447
+ let line = 1;
1448
+ for (let i = 0; i < index; i++) {
1449
+ if (xml[i] === "\n") line++;
1450
+ }
1451
+ return line;
1452
+ }
1453
+ function extractSitemapTag(block, tag) {
1454
+ const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
1455
+ return m ? m[1].trim() : void 0;
1456
+ }
1457
+ function isValidW3CDatetime(s) {
1458
+ return /^\d{4}(-\d{2}(-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?)?)?$/.test(s);
1459
+ }
1460
+ function isAbsoluteHttpUrl(s) {
1461
+ return /^https?:\/\//i.test(s);
1462
+ }
1463
+ function validateSitemap(xml) {
1464
+ if (!xml || !xml.trim()) return { error: "Input is empty" };
1465
+ const trimmed = xml.trim();
1466
+ const issues = [];
1467
+ if (!/^\s*<\?xml/i.test(trimmed)) {
1468
+ issues.push({ severity: "warning", line: 1, message: 'Missing XML declaration (<?xml version="1.0" encoding="UTF-8"?>)' });
1469
+ }
1470
+ const isUrlset = /<urlset[\s>]/i.test(trimmed);
1471
+ const isSitemapIndex = /<sitemapindex[\s>]/i.test(trimmed);
1472
+ if (!isUrlset && !isSitemapIndex) {
1473
+ return { error: "Not a valid sitemap: root element must be <urlset> or <sitemapindex>" };
1474
+ }
1475
+ if (isUrlset && isSitemapIndex) {
1476
+ return { error: "Ambiguous file: contains both <urlset> and <sitemapindex> root elements" };
1477
+ }
1478
+ const type = isUrlset ? "urlset" : "sitemapindex";
1479
+ if (isUrlset && !/xmlns\s*=\s*["']http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9["']/i.test(trimmed)) {
1480
+ issues.push({ severity: "warning", line: 1, message: "Missing or non-standard xmlns namespace (expected http://www.sitemaps.org/schemas/sitemap/0.9)" });
1481
+ }
1482
+ const blockTag = isUrlset ? "url" : "sitemap";
1483
+ const blockRe = new RegExp(`<${blockTag}>([\\s\\S]*?)</${blockTag}>`, "gi");
1484
+ const seenLocs = /* @__PURE__ */ new Set();
1485
+ const entries = [];
1486
+ let match;
1487
+ while ((match = blockRe.exec(trimmed)) !== null) {
1488
+ const block = match[1];
1489
+ const line = sitemapLineNumber(trimmed, match.index);
1490
+ const loc = extractSitemapTag(block, "loc");
1491
+ if (!loc) {
1492
+ issues.push({ severity: "error", line, message: `<${blockTag}> entry is missing required <loc>` });
1493
+ continue;
1494
+ }
1495
+ if (!isAbsoluteHttpUrl(loc)) {
1496
+ issues.push({ severity: "error", line, message: `<loc>${loc}</loc> must be an absolute URL starting with http:// or https://` });
1497
+ }
1498
+ if (/&(?!amp;|lt;|gt;|apos;|quot;|#\d+;)/.test(loc)) {
1499
+ issues.push({ severity: "error", line, message: `<loc>${loc}</loc> contains an unescaped '&' (use &amp; instead)` });
1500
+ }
1501
+ if (seenLocs.has(loc)) {
1502
+ issues.push({ severity: "warning", line, message: `Duplicate <loc>${loc}</loc>` });
1503
+ }
1504
+ seenLocs.add(loc);
1505
+ const lastmod = extractSitemapTag(block, "lastmod");
1506
+ if (lastmod !== void 0 && !isValidW3CDatetime(lastmod)) {
1507
+ issues.push({ severity: "warning", line, message: `<lastmod>${lastmod}</lastmod> is not a valid W3C datetime` });
1508
+ }
1509
+ if (type === "urlset") {
1510
+ const changefreq = extractSitemapTag(block, "changefreq");
1511
+ if (changefreq !== void 0 && !VALID_CHANGEFREQ.has(changefreq.toLowerCase())) {
1512
+ issues.push({ severity: "error", line, message: `<changefreq>${changefreq}</changefreq> must be one of: ${[...VALID_CHANGEFREQ].join(", ")}` });
1513
+ }
1514
+ const priority = extractSitemapTag(block, "priority");
1515
+ if (priority !== void 0) {
1516
+ const p = Number(priority);
1517
+ if (Number.isNaN(p) || p < 0 || p > 1) {
1518
+ issues.push({ severity: "error", line, message: `<priority>${priority}</priority> must be a number between 0.0 and 1.0` });
1519
+ }
1520
+ }
1521
+ entries.push({ loc, lastmod, changefreq, priority, line });
1522
+ } else {
1523
+ entries.push({ loc, lastmod, line });
1524
+ }
1525
+ }
1526
+ if (entries.length === 0) {
1527
+ issues.push({ severity: "error", line: 1, message: `No <${blockTag}> entries found` });
1528
+ }
1529
+ if (type === "urlset" && entries.length > 5e4) {
1530
+ issues.push({ severity: "error", line: 1, message: `Sitemap contains ${entries.length} URLs, exceeding the 50,000 URL limit per the sitemaps.org spec` });
1531
+ }
1532
+ const valid = !issues.some((i) => i.severity === "error");
1533
+ return { type, urlCount: entries.length, entries, issues, valid };
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
+ }
1443
1618
 
1444
- 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 };
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 };
@@ -60,6 +60,7 @@ __export(units_exports, {
60
60
  sortVersions: () => sortVersions,
61
61
  splitSubnet: () => splitSubnet,
62
62
  symbolicToPermissions: () => symbolicToPermissions,
63
+ validateCardNumber: () => validateCardNumber,
63
64
  viewportToPx: () => viewportToPx
64
65
  });
65
66
  function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
@@ -892,5 +893,65 @@ function sortVersions(versions, order = "asc") {
892
893
  return order === "asc" ? cmp : -cmp;
893
894
  });
894
895
  }
896
+ var CARD_NETWORK_RULES = [
897
+ { name: "American Express", lengths: [15], test: (d) => /^3[47]/.test(d) },
898
+ { name: "Diners Club", lengths: [14], test: (d) => /^3(?:0[0-5]|[68])/.test(d) },
899
+ {
900
+ name: "Discover",
901
+ lengths: [16, 19],
902
+ 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)
903
+ },
904
+ { name: "JCB", lengths: [16], test: (d) => /^35(?:2[89]|[3-8]\d)/.test(d) },
905
+ {
906
+ name: "Mastercard",
907
+ lengths: [16],
908
+ test: (d) => {
909
+ const two = parseInt(d.slice(0, 2), 10);
910
+ const four = parseInt(d.slice(0, 4), 10);
911
+ return two >= 51 && two <= 55 || four >= 2221 && four <= 2720;
912
+ }
913
+ },
914
+ { name: "Maestro", lengths: [12, 13, 14, 15, 16, 17, 18, 19], test: (d) => /^(?:50|5[6-8]|6304|6390|67)/.test(d) },
915
+ { name: "UnionPay", lengths: [16, 17, 18, 19], test: (d) => /^62/.test(d) },
916
+ { name: "Visa", lengths: [13, 16, 19], test: (d) => /^4/.test(d) }
917
+ ];
918
+ function luhnCheck(digits) {
919
+ let sum = 0;
920
+ let shouldDouble = false;
921
+ for (let i = digits.length - 1; i >= 0; i--) {
922
+ let d = parseInt(digits[i], 10);
923
+ if (shouldDouble) {
924
+ d *= 2;
925
+ if (d > 9) d -= 9;
926
+ }
927
+ sum += d;
928
+ shouldDouble = !shouldDouble;
929
+ }
930
+ return sum % 10 === 0;
931
+ }
932
+ function detectCardNetwork(digits) {
933
+ for (const rule of CARD_NETWORK_RULES) {
934
+ if (rule.lengths.includes(digits.length) && rule.test(digits)) return rule.name;
935
+ }
936
+ return null;
937
+ }
938
+ function formatCardNumber(digits, network) {
939
+ if (network === "American Express") {
940
+ return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10)}`.trim();
941
+ }
942
+ return digits.match(/.{1,4}/g)?.join(" ") ?? digits;
943
+ }
944
+ function validateCardNumber(input) {
945
+ if (!input || !input.trim()) return { error: "Input is empty" };
946
+ const digits = input.replace(/[\s-]/g, "");
947
+ if (!/^\d+$/.test(digits)) return { error: "Card number must contain only digits, spaces, or hyphens" };
948
+ if (digits.length < 12 || digits.length > 19) {
949
+ return { error: `Card number length (${digits.length}) is outside the valid range of 12-19 digits` };
950
+ }
951
+ const valid = luhnCheck(digits);
952
+ const network = detectCardNetwork(digits);
953
+ const formatted = formatCardNumber(digits, network);
954
+ return { valid, network, length: digits.length, formatted };
955
+ }
895
956
 
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 };
957
+ 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, validateCardNumber, viewportToPx };
package/dist/index.cjs CHANGED
@@ -6665,6 +6665,7 @@ __export(units_exports, {
6665
6665
  sortVersions: () => sortVersions,
6666
6666
  splitSubnet: () => splitSubnet,
6667
6667
  symbolicToPermissions: () => symbolicToPermissions,
6668
+ validateCardNumber: () => validateCardNumber,
6668
6669
  viewportToPx: () => viewportToPx
6669
6670
  });
6670
6671
  function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
@@ -7497,6 +7498,66 @@ function sortVersions(versions, order = "asc") {
7497
7498
  return order === "asc" ? cmp : -cmp;
7498
7499
  });
7499
7500
  }
7501
+ var CARD_NETWORK_RULES = [
7502
+ { name: "American Express", lengths: [15], test: (d) => /^3[47]/.test(d) },
7503
+ { name: "Diners Club", lengths: [14], test: (d) => /^3(?:0[0-5]|[68])/.test(d) },
7504
+ {
7505
+ name: "Discover",
7506
+ lengths: [16, 19],
7507
+ 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)
7508
+ },
7509
+ { name: "JCB", lengths: [16], test: (d) => /^35(?:2[89]|[3-8]\d)/.test(d) },
7510
+ {
7511
+ name: "Mastercard",
7512
+ lengths: [16],
7513
+ test: (d) => {
7514
+ const two = parseInt(d.slice(0, 2), 10);
7515
+ const four = parseInt(d.slice(0, 4), 10);
7516
+ return two >= 51 && two <= 55 || four >= 2221 && four <= 2720;
7517
+ }
7518
+ },
7519
+ { name: "Maestro", lengths: [12, 13, 14, 15, 16, 17, 18, 19], test: (d) => /^(?:50|5[6-8]|6304|6390|67)/.test(d) },
7520
+ { name: "UnionPay", lengths: [16, 17, 18, 19], test: (d) => /^62/.test(d) },
7521
+ { name: "Visa", lengths: [13, 16, 19], test: (d) => /^4/.test(d) }
7522
+ ];
7523
+ function luhnCheck(digits) {
7524
+ let sum = 0;
7525
+ let shouldDouble = false;
7526
+ for (let i = digits.length - 1; i >= 0; i--) {
7527
+ let d = parseInt(digits[i], 10);
7528
+ if (shouldDouble) {
7529
+ d *= 2;
7530
+ if (d > 9) d -= 9;
7531
+ }
7532
+ sum += d;
7533
+ shouldDouble = !shouldDouble;
7534
+ }
7535
+ return sum % 10 === 0;
7536
+ }
7537
+ function detectCardNetwork(digits) {
7538
+ for (const rule of CARD_NETWORK_RULES) {
7539
+ if (rule.lengths.includes(digits.length) && rule.test(digits)) return rule.name;
7540
+ }
7541
+ return null;
7542
+ }
7543
+ function formatCardNumber(digits, network) {
7544
+ if (network === "American Express") {
7545
+ return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10)}`.trim();
7546
+ }
7547
+ return digits.match(/.{1,4}/g)?.join(" ") ?? digits;
7548
+ }
7549
+ function validateCardNumber(input) {
7550
+ if (!input || !input.trim()) return { error: "Input is empty" };
7551
+ const digits = input.replace(/[\s-]/g, "");
7552
+ if (!/^\d+$/.test(digits)) return { error: "Card number must contain only digits, spaces, or hyphens" };
7553
+ if (digits.length < 12 || digits.length > 19) {
7554
+ return { error: `Card number length (${digits.length}) is outside the valid range of 12-19 digits` };
7555
+ }
7556
+ const valid = luhnCheck(digits);
7557
+ const network = detectCardNetwork(digits);
7558
+ const formatted = formatCardNumber(digits, network);
7559
+ return { valid, network, length: digits.length, formatted };
7560
+ }
7500
7561
 
7501
7562
  // src/tools/network.ts
7502
7563
  var network_exports = {};
@@ -7530,7 +7591,9 @@ __export(network_exports, {
7530
7591
  typeNumberToLabel: () => typeNumberToLabel,
7531
7592
  validateDomain: () => validateDomain,
7532
7593
  validateHeaderName: () => validateHeaderName,
7533
- validateHeaderValue: () => validateHeaderValue
7594
+ validateHeaderValue: () => validateHeaderValue,
7595
+ validateRobotsTxt: () => validateRobotsTxt,
7596
+ validateSitemap: () => validateSitemap
7534
7597
  });
7535
7598
  var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SOA", "PTR", "SRV", "CAA"];
7536
7599
  function buildDnsQueryUrl(domain, type) {
@@ -8938,6 +9001,179 @@ function parseHar(input) {
8938
9001
  summary: { totalRequests, totalSize, totalTime, avgTime, failedRequests, methods, statusCodes }
8939
9002
  };
8940
9003
  }
9004
+ var VALID_CHANGEFREQ = /* @__PURE__ */ new Set(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]);
9005
+ function sitemapLineNumber(xml, index) {
9006
+ let line = 1;
9007
+ for (let i = 0; i < index; i++) {
9008
+ if (xml[i] === "\n") line++;
9009
+ }
9010
+ return line;
9011
+ }
9012
+ function extractSitemapTag(block, tag) {
9013
+ const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
9014
+ return m ? m[1].trim() : void 0;
9015
+ }
9016
+ function isValidW3CDatetime(s) {
9017
+ return /^\d{4}(-\d{2}(-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?)?)?$/.test(s);
9018
+ }
9019
+ function isAbsoluteHttpUrl(s) {
9020
+ return /^https?:\/\//i.test(s);
9021
+ }
9022
+ function validateSitemap(xml) {
9023
+ if (!xml || !xml.trim()) return { error: "Input is empty" };
9024
+ const trimmed = xml.trim();
9025
+ const issues = [];
9026
+ if (!/^\s*<\?xml/i.test(trimmed)) {
9027
+ issues.push({ severity: "warning", line: 1, message: 'Missing XML declaration (<?xml version="1.0" encoding="UTF-8"?>)' });
9028
+ }
9029
+ const isUrlset = /<urlset[\s>]/i.test(trimmed);
9030
+ const isSitemapIndex = /<sitemapindex[\s>]/i.test(trimmed);
9031
+ if (!isUrlset && !isSitemapIndex) {
9032
+ return { error: "Not a valid sitemap: root element must be <urlset> or <sitemapindex>" };
9033
+ }
9034
+ if (isUrlset && isSitemapIndex) {
9035
+ return { error: "Ambiguous file: contains both <urlset> and <sitemapindex> root elements" };
9036
+ }
9037
+ const type = isUrlset ? "urlset" : "sitemapindex";
9038
+ if (isUrlset && !/xmlns\s*=\s*["']http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9["']/i.test(trimmed)) {
9039
+ issues.push({ severity: "warning", line: 1, message: "Missing or non-standard xmlns namespace (expected http://www.sitemaps.org/schemas/sitemap/0.9)" });
9040
+ }
9041
+ const blockTag = isUrlset ? "url" : "sitemap";
9042
+ const blockRe = new RegExp(`<${blockTag}>([\\s\\S]*?)</${blockTag}>`, "gi");
9043
+ const seenLocs = /* @__PURE__ */ new Set();
9044
+ const entries = [];
9045
+ let match;
9046
+ while ((match = blockRe.exec(trimmed)) !== null) {
9047
+ const block = match[1];
9048
+ const line = sitemapLineNumber(trimmed, match.index);
9049
+ const loc = extractSitemapTag(block, "loc");
9050
+ if (!loc) {
9051
+ issues.push({ severity: "error", line, message: `<${blockTag}> entry is missing required <loc>` });
9052
+ continue;
9053
+ }
9054
+ if (!isAbsoluteHttpUrl(loc)) {
9055
+ issues.push({ severity: "error", line, message: `<loc>${loc}</loc> must be an absolute URL starting with http:// or https://` });
9056
+ }
9057
+ if (/&(?!amp;|lt;|gt;|apos;|quot;|#\d+;)/.test(loc)) {
9058
+ issues.push({ severity: "error", line, message: `<loc>${loc}</loc> contains an unescaped '&' (use &amp; instead)` });
9059
+ }
9060
+ if (seenLocs.has(loc)) {
9061
+ issues.push({ severity: "warning", line, message: `Duplicate <loc>${loc}</loc>` });
9062
+ }
9063
+ seenLocs.add(loc);
9064
+ const lastmod = extractSitemapTag(block, "lastmod");
9065
+ if (lastmod !== void 0 && !isValidW3CDatetime(lastmod)) {
9066
+ issues.push({ severity: "warning", line, message: `<lastmod>${lastmod}</lastmod> is not a valid W3C datetime` });
9067
+ }
9068
+ if (type === "urlset") {
9069
+ const changefreq = extractSitemapTag(block, "changefreq");
9070
+ if (changefreq !== void 0 && !VALID_CHANGEFREQ.has(changefreq.toLowerCase())) {
9071
+ issues.push({ severity: "error", line, message: `<changefreq>${changefreq}</changefreq> must be one of: ${[...VALID_CHANGEFREQ].join(", ")}` });
9072
+ }
9073
+ const priority = extractSitemapTag(block, "priority");
9074
+ if (priority !== void 0) {
9075
+ const p = Number(priority);
9076
+ if (Number.isNaN(p) || p < 0 || p > 1) {
9077
+ issues.push({ severity: "error", line, message: `<priority>${priority}</priority> must be a number between 0.0 and 1.0` });
9078
+ }
9079
+ }
9080
+ entries.push({ loc, lastmod, changefreq, priority, line });
9081
+ } else {
9082
+ entries.push({ loc, lastmod, line });
9083
+ }
9084
+ }
9085
+ if (entries.length === 0) {
9086
+ issues.push({ severity: "error", line: 1, message: `No <${blockTag}> entries found` });
9087
+ }
9088
+ if (type === "urlset" && entries.length > 5e4) {
9089
+ issues.push({ severity: "error", line: 1, message: `Sitemap contains ${entries.length} URLs, exceeding the 50,000 URL limit per the sitemaps.org spec` });
9090
+ }
9091
+ const valid = !issues.some((i) => i.severity === "error");
9092
+ return { type, urlCount: entries.length, entries, issues, valid };
9093
+ }
9094
+ var KNOWN_ROBOTS_DIRECTIVES = /* @__PURE__ */ new Set(["user-agent", "disallow", "allow", "sitemap", "crawl-delay", "host"]);
9095
+ function validateRobotsTxt(text) {
9096
+ if (!text || !text.trim()) return { error: "Input is empty" };
9097
+ const lines = text.split("\n");
9098
+ const issues = [];
9099
+ const groups = [];
9100
+ const sitemaps = [];
9101
+ let currentGroup = null;
9102
+ for (let i = 0; i < lines.length; i++) {
9103
+ const rawLine = lines[i];
9104
+ const lineNum = i + 1;
9105
+ const commentIdx = rawLine.indexOf("#");
9106
+ const line = (commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine).trim();
9107
+ if (!line) continue;
9108
+ const colonIdx = line.indexOf(":");
9109
+ if (colonIdx === -1) {
9110
+ issues.push({ severity: "error", line: lineNum, message: `Malformed line (missing ':'): "${line}"` });
9111
+ continue;
9112
+ }
9113
+ const directive = line.slice(0, colonIdx).trim().toLowerCase();
9114
+ const value = line.slice(colonIdx + 1).trim();
9115
+ if (!KNOWN_ROBOTS_DIRECTIVES.has(directive)) {
9116
+ issues.push({ severity: "warning", line: lineNum, message: `Unknown directive "${directive}"` });
9117
+ continue;
9118
+ }
9119
+ if (directive === "sitemap") {
9120
+ if (!/^https?:\/\//i.test(value)) {
9121
+ issues.push({ severity: "error", line: lineNum, message: `Sitemap URL "${value}" must be absolute (start with http:// or https://)` });
9122
+ }
9123
+ sitemaps.push(value);
9124
+ continue;
9125
+ }
9126
+ if (directive === "user-agent") {
9127
+ if (!value) {
9128
+ issues.push({ severity: "error", line: lineNum, message: "User-agent directive is missing a value" });
9129
+ }
9130
+ if (currentGroup && currentGroup.rules.length === 0 && currentGroup.crawlDelay === void 0) {
9131
+ currentGroup.userAgents.push(value);
9132
+ } else {
9133
+ currentGroup = { userAgents: [value], rules: [], line: lineNum };
9134
+ groups.push(currentGroup);
9135
+ }
9136
+ continue;
9137
+ }
9138
+ if (!currentGroup) {
9139
+ issues.push({ severity: "error", line: lineNum, message: `"${directive}" directive found before any User-agent line` });
9140
+ continue;
9141
+ }
9142
+ if (directive === "disallow" || directive === "allow") {
9143
+ if (value !== "" && !value.startsWith("/") && !value.startsWith("*")) {
9144
+ issues.push({ severity: "warning", line: lineNum, message: `${directive === "disallow" ? "Disallow" : "Allow"} value "${value}" should start with "/" (or be empty)` });
9145
+ }
9146
+ if (currentGroup.rules.some((r) => r.type === directive && r.value === value)) {
9147
+ issues.push({ severity: "warning", line: lineNum, message: `Duplicate ${directive} rule for "${value}" in this group` });
9148
+ }
9149
+ currentGroup.rules.push({ type: directive, value, line: lineNum });
9150
+ } else if (directive === "crawl-delay") {
9151
+ const n = Number(value);
9152
+ if (Number.isNaN(n) || n < 0) {
9153
+ issues.push({ severity: "error", line: lineNum, message: `Crawl-delay "${value}" must be a non-negative number` });
9154
+ } else {
9155
+ currentGroup.crawlDelay = n;
9156
+ }
9157
+ }
9158
+ }
9159
+ if (groups.length === 0) {
9160
+ issues.push({ severity: "error", line: 1, message: "No User-agent groups found" });
9161
+ }
9162
+ const seenUA = /* @__PURE__ */ new Map();
9163
+ for (const g of groups) {
9164
+ for (const ua of g.userAgents) {
9165
+ const key = ua.toLowerCase();
9166
+ const firstLine = seenUA.get(key);
9167
+ if (firstLine !== void 0) {
9168
+ issues.push({ severity: "warning", line: g.line, message: `Duplicate User-agent "${ua}" (first seen on line ${firstLine})` });
9169
+ } else {
9170
+ seenUA.set(key, g.line);
9171
+ }
9172
+ }
9173
+ }
9174
+ const valid = !issues.some((i) => i.severity === "error");
9175
+ return { groups, sitemaps, issues, valid };
9176
+ }
8941
9177
 
8942
9178
  // src/tools/api.ts
8943
9179
  var api_exports = {};
package/dist/index.d.cts CHANGED
@@ -5,8 +5,8 @@ export { t as text } from './text-Bbx-tZ43.cjs';
5
5
  export { d as data } from './data-D8XTI7Du.cjs';
6
6
  export { g as generators } from './generators-BGtRGpJZ.cjs';
7
7
  export { t as time } from './time-DbT8fjaF.cjs';
8
- export { u as units } from './units-6lwDYBvX.cjs';
9
- export { n as network } from './network-DwyAjwJS.cjs';
8
+ export { u as units } from './units-DRT6W-vu.cjs';
9
+ export { n as network } from './network-DaNoZ5ER.cjs';
10
10
  export { a as api } from './api-Xl7OzIgq.cjs';
11
11
  export { c as code } from './code-BUqyaofO.cjs';
12
12
  export { c as color } from './color-tPwZCr9H.cjs';
package/dist/index.d.ts CHANGED
@@ -5,8 +5,8 @@ export { t as text } from './text-Bbx-tZ43.js';
5
5
  export { d as data } from './data-D8XTI7Du.js';
6
6
  export { g as generators } from './generators-BGtRGpJZ.js';
7
7
  export { t as time } from './time-DbT8fjaF.js';
8
- export { u as units } from './units-6lwDYBvX.js';
9
- export { n as network } from './network-DwyAjwJS.js';
8
+ export { u as units } from './units-DRT6W-vu.js';
9
+ export { n as network } from './network-DaNoZ5ER.js';
10
10
  export { a as api } from './api-Xl7OzIgq.js';
11
11
  export { c as code } from './code-BUqyaofO.js';
12
12
  export { c as color } from './color-tPwZCr9H.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { media_exports as media } from './chunk-GUUYEIU6.js';
2
- export { units_exports as units } from './chunk-QJE743LY.js';
3
- export { network_exports as network } from './chunk-V5JKVDBA.js';
2
+ export { units_exports as units } from './chunk-YVFM5WYI.js';
3
+ export { network_exports as network } from './chunk-OB56VNKJ.js';
4
4
  export { api_exports as api } from './chunk-Q3B3YWOA.js';
5
5
  export { code_exports as code } from './chunk-V5S6OJ4A.js';
6
6
  export { color_exports as color } from './chunk-BPVAB4P2.js';