@utilix-tech/sdk 0.60.0 → 0.62.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,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.62.0
6
+ - Add `buildPriorityHeader`, `parsePriorityHeader` (priority-header): build an HTTP Priority request/response-header value from an urgency (0-7) and/or an incremental flag per RFC 9218, or parse an existing one into the same shape.
7
+ - Add `lookupCloseCode`, `listCloseCodes` (websocket-close-codes): look up what a WebSocket close code means per RFC 6455 and the IANA reserved ranges, or list every well-known code.
8
+
9
+ ## 0.61.0
10
+ - Add `calculateExtraPaymentPayoff` (extra-payment-mortgage-calculator): compute how much faster a fixed-rate loan pays off, and how much interest is saved, when a fixed extra amount is added to every monthly payment.
11
+
5
12
  ## 0.60.0
6
13
  - Add `buildRetryAfterHeader`, `parseRetryAfterHeader` (retry-after-builder): build an HTTP Retry-After response header from a delay in seconds or a target date, or parse an existing one, per RFC 9110.
7
14
  - Add `calculateFreelancerRate` (freelancer-rate-calculator): solve for the minimum hourly rate a freelancer must charge to hit a target annual income, given billable hours, weeks worked, overhead, and a profit margin.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **665 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **670 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)
@@ -343,7 +343,7 @@ formatIso8601Duration(90061); // { iso: "P1DT1H1M1S", ... }
343
343
  ### `/units`: Conversions
344
344
 
345
345
  ```ts
346
- import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72, calculateEmi, calculateRoi, compareUnitPrices, calculatePaybackPeriod, calculateCdMaturity, calculateDepreciationSchedule, calculateCashRounding, calculateMovingAverages, calculateGpa, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateBlendedLoanRate, calculateFreelancerRate } from "@utilix-tech/sdk/units";
346
+ import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72, calculateEmi, calculateRoi, compareUnitPrices, calculatePaybackPeriod, calculateCdMaturity, calculateDepreciationSchedule, calculateCashRounding, calculateMovingAverages, calculateGpa, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateBlendedLoanRate, calculateFreelancerRate, calculateExtraPaymentPayoff } from "@utilix-tech/sdk/units";
347
347
 
348
348
  // File sizes
349
349
  convertBytes(1073741824, "GB"); // 1
@@ -489,6 +489,10 @@ calculateDebtToIncomeRatio([{ label: "Rent", amount: 1500 }, { label: "Car loan"
489
489
  // Minimum hourly rate to hit a target annual income, given billable hours, weeks worked, overhead, and margin
490
490
  calculateFreelancerRate({ targetAnnualIncome: 80000, billableHoursPerWeek: 25, weeksPerYear: 48 });
491
491
  // { hourlyRate: 66.67, dailyRate: 533.33, requiredAnnualRevenue: 80000, annualBillableHours: 1200, monthlyRevenueTarget: 6666.67 }
492
+
493
+ // How much sooner a loan pays off, and how much interest is saved, with a fixed extra monthly payment
494
+ calculateExtraPaymentPayoff(200000, 6, 360, 200);
495
+ // { monthlyPayment: 1199.1, newPayoffMonths: 252, monthsSaved: 108, interestSaved: 79800.86, ... }
492
496
  ```
493
497
 
494
498
  ---
@@ -639,6 +643,22 @@ buildRetryAfterHeader({ seconds: 120 });
639
643
  // Parse an existing Retry-After header value into the same shape
640
644
  parseRetryAfterHeader("Retry-After: 120");
641
645
  // { header: "Retry-After: 120", headerValue: "120", type: "seconds", seconds: 120 }
646
+
647
+ // Build a Priority request/response header from urgency (0-7) and/or incremental, per RFC 9218
648
+ buildPriorityHeader({ urgency: 1, incremental: true });
649
+ // { header: "Priority: u=1, i", headerValue: "u=1, i", urgency: 1, incremental: true }
650
+
651
+ // Parse an existing Priority header value into the same shape, with spec defaults applied
652
+ parsePriorityHeader("u=2");
653
+ // { header: "Priority: u=2, i=?0", headerValue: "u=2, i=?0", urgency: 2, incremental: false }
654
+
655
+ // Look up what a WebSocket close code means, per RFC 6455 and the IANA reserved ranges
656
+ lookupCloseCode(1006);
657
+ // { code: 1006, name: "Abnormal Closure", description: "...", range: "standard", sentOnWire: false }
658
+
659
+ // List every well-known WebSocket close code (1000-1015)
660
+ listCloseCodes();
661
+ // [{ code: 1000, name: "Normal Closure", ... }, ...]
642
662
  ```
643
663
 
644
664
  ---
@@ -898,7 +918,7 @@ inspectGifFrames(new Uint8Array(gifBytes));
898
918
  | `@utilix-tech/sdk/generators` | UUID v4/v7, ULID, passwords, strength check, entropy estimator, fake data, CSP nonce generator |
899
919
  | `@utilix-tech/sdk/time` | Date diff, timezone convert, cron parse, relative time |
900
920
  | `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode/ISBN validation, loan calculator, EMI calculator, compound/simple interest calculators, VAT/sales tax calculator, tip/bill split calculator, break-even point calculator, Rule of 72 calculator, savings goal calculator, ROI calculator, cash denomination/till calculator, payback period calculator, CD maturity calculator, debt-to-income ratio calculator, freelancer hourly rate calculator |
901
- | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing, IPv6 address compress/expand, Permissions-Policy header builder, Client Hints header builder/parser, Content-Disposition header builder/parser, X-Content-Type-Options header builder/risk assessor, sitemap.xml validator, sitemap index splitter, Prefer header builder/parser (RFC 7240), Retry-After header builder/parser (RFC 9110) |
921
+ | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing, IPv6 address compress/expand, Permissions-Policy header builder, Client Hints header builder/parser, Content-Disposition header builder/parser, X-Content-Type-Options header builder/risk assessor, sitemap.xml validator, sitemap index splitter, Prefer header builder/parser (RFC 7240), Retry-After header builder/parser (RFC 9110), Priority header builder/parser (RFC 9218), WebSocket close code reference/lookup |
902
922
  | `@utilix-tech/sdk/api` | cURL build/parse, JWT decode/sign, JWKS parse, CORS headers, CSP header |
903
923
  | `@utilix-tech/sdk/code` | SQL format/minify, HTML format/minify, regex tester, GraphQL format, MIME type lookup, GitHub URL parsing, regex ReDoS detector, Conventional Commits linter, package.json exports field validator |
904
924
  | `@utilix-tech/sdk/color` | Hex/RGB/HSL/HSV/CMYK convert, palette generation, contrast check |
@@ -23,6 +23,7 @@ __export(network_exports, {
23
23
  buildMapUrl: () => buildMapUrl,
24
24
  buildPermissionsPolicy: () => buildPermissionsPolicy,
25
25
  buildPreferHeader: () => buildPreferHeader,
26
+ buildPriorityHeader: () => buildPriorityHeader,
26
27
  buildRangeHeader: () => buildRangeHeader,
27
28
  buildReferrerPolicy: () => buildReferrerPolicy,
28
29
  buildRetryAfterHeader: () => buildRetryAfterHeader,
@@ -43,6 +44,8 @@ __export(network_exports, {
43
44
  isValidIp: () => isValidIp,
44
45
  isValidIpv4: () => isValidIpv4,
45
46
  isValidIpv6: () => isValidIpv6,
47
+ listCloseCodes: () => listCloseCodes,
48
+ lookupCloseCode: () => lookupCloseCode,
46
49
  parseAcceptLanguage: () => parseAcceptLanguage,
47
50
  parseClientHintHeaders: () => parseClientHintHeaders,
48
51
  parseContentDisposition: () => parseContentDisposition,
@@ -53,6 +56,7 @@ __export(network_exports, {
53
56
  parseIpv6Address: () => parseIpv6Address,
54
57
  parsePermissionsPolicy: () => parsePermissionsPolicy,
55
58
  parsePreferHeader: () => parsePreferHeader,
59
+ parsePriorityHeader: () => parsePriorityHeader,
56
60
  parseRetryAfterHeader: () => parseRetryAfterHeader,
57
61
  parseUserAgent: () => parseUserAgent,
58
62
  parseVaryHeader: () => parseVaryHeader,
@@ -3019,5 +3023,96 @@ function parseRetryAfterHeader(raw) {
3019
3023
  const httpDate = parsed.toUTCString();
3020
3024
  return { header: `Retry-After: ${httpDate}`, headerValue: httpDate, type: "http-date", httpDate, iso: parsed.toISOString() };
3021
3025
  }
3026
+ function isValidUrgency(n) {
3027
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 7;
3028
+ }
3029
+ function buildPriorityHeader(input) {
3030
+ const { urgency, incremental } = input ?? {};
3031
+ if (urgency === void 0 && incremental === void 0) {
3032
+ return { error: "Provide at least one of urgency or incremental" };
3033
+ }
3034
+ if (urgency !== void 0 && !isValidUrgency(urgency)) {
3035
+ return { error: "urgency must be an integer between 0 and 7" };
3036
+ }
3037
+ const parts = [];
3038
+ if (urgency !== void 0) parts.push(`u=${urgency}`);
3039
+ if (incremental === true) parts.push("i");
3040
+ else if (incremental === false) parts.push("i=?0");
3041
+ const headerValue = parts.join(", ");
3042
+ return {
3043
+ header: `Priority: ${headerValue}`,
3044
+ headerValue,
3045
+ urgency: urgency ?? 3,
3046
+ incremental: incremental ?? false
3047
+ };
3048
+ }
3049
+ function parsePriorityHeader(raw) {
3050
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
3051
+ const withoutLabel = raw.trim().replace(/^priority:\s*/i, "");
3052
+ const parts = withoutLabel.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
3053
+ if (parts.length === 0) return { error: "Input is empty" };
3054
+ let urgency = 3;
3055
+ let incremental = false;
3056
+ for (const part of parts) {
3057
+ const eq = part.indexOf("=");
3058
+ const key = (eq === -1 ? part : part.slice(0, eq)).trim();
3059
+ const rawValue = eq === -1 ? void 0 : part.slice(eq + 1).trim();
3060
+ if (key === "u") {
3061
+ if (rawValue === void 0 || !/^\d+$/.test(rawValue)) return { error: `Invalid urgency value in "${part}"` };
3062
+ const n = Number(rawValue);
3063
+ if (n < 0 || n > 7) return { error: `urgency must be between 0 and 7, got ${n}` };
3064
+ urgency = n;
3065
+ } else if (key === "i") {
3066
+ if (rawValue === void 0 || rawValue === "?1") incremental = true;
3067
+ else if (rawValue === "?0") incremental = false;
3068
+ else return { error: `Invalid incremental value in "${part}"` };
3069
+ }
3070
+ }
3071
+ const headerValue = incremental ? `u=${urgency}, i` : `u=${urgency}, i=?0`;
3072
+ return { header: `Priority: ${headerValue}`, headerValue, urgency, incremental };
3073
+ }
3074
+ var WELL_KNOWN_CLOSE_CODES = {
3075
+ 1e3: { name: "Normal Closure", description: "The connection successfully completed the purpose for which it was created.", sentOnWire: true },
3076
+ 1001: { name: "Going Away", description: "An endpoint is going away, such as a server shutting down or a browser navigating away from the page.", sentOnWire: true },
3077
+ 1002: { name: "Protocol Error", description: "An endpoint is terminating the connection due to a protocol error.", sentOnWire: true },
3078
+ 1003: { name: "Unsupported Data", description: "An endpoint received a data type it cannot accept, such as a binary-only endpoint receiving a text message.", sentOnWire: true },
3079
+ 1004: { name: "Reserved", description: "Reserved. A meaning might be defined in the future.", sentOnWire: true },
3080
+ 1005: { name: "No Status Received", description: "A reserved value used internally by libraries to indicate no status code was actually present in the close frame. Must not be sent on the wire.", sentOnWire: false },
3081
+ 1006: { name: "Abnormal Closure", description: "A reserved value used internally by libraries to indicate the connection closed without a close frame, e.g. the TCP connection dropped. Must not be sent on the wire.", sentOnWire: false },
3082
+ 1007: { name: "Invalid frame payload data", description: "An endpoint is terminating the connection because it received data inconsistent with the message type, such as non-UTF-8 data in a text message.", sentOnWire: true },
3083
+ 1008: { name: "Policy Violation", description: "A generic status code used when no other code fits and the endpoint does not want to reveal further details, or a message violated its policy.", sentOnWire: true },
3084
+ 1009: { name: "Message Too Big", description: "An endpoint is terminating the connection because it received a message too large to process.", sentOnWire: true },
3085
+ 1010: { name: "Mandatory Extension", description: "A client is terminating the connection because the server did not negotiate one or more extensions the client required.", sentOnWire: true },
3086
+ 1011: { name: "Internal Error", description: "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.", sentOnWire: true },
3087
+ 1012: { name: "Service Restart", description: "The server is terminating the connection because it is restarting.", sentOnWire: true },
3088
+ 1013: { name: "Try Again Later", description: "The server is terminating the connection due to a temporary condition, such as being overloaded, and the client should reconnect later.", sentOnWire: true },
3089
+ 1014: { name: "Bad Gateway", description: "The server, acting as a gateway or proxy, received an invalid response from an upstream server it connected to while processing the request.", sentOnWire: true },
3090
+ 1015: { name: "TLS Handshake", description: "A reserved value used internally by libraries to indicate the TLS handshake failed. Must not be sent on the wire.", sentOnWire: false }
3091
+ };
3092
+ function listCloseCodes() {
3093
+ return Object.entries(WELL_KNOWN_CLOSE_CODES).map(([code, info]) => ({ code: Number(code), name: info.name, description: info.description, range: "standard", sentOnWire: info.sentOnWire })).sort((a, b) => a.code - b.code);
3094
+ }
3095
+ function lookupCloseCode(code) {
3096
+ if (typeof code !== "number" || !Number.isInteger(code) || code < 0 || code > 65535) {
3097
+ return { error: "code must be an integer between 0 and 65535" };
3098
+ }
3099
+ const known = WELL_KNOWN_CLOSE_CODES[code];
3100
+ if (known) {
3101
+ return { code, name: known.name, description: known.description, range: "standard", sentOnWire: known.sentOnWire };
3102
+ }
3103
+ if (code < 1e3) {
3104
+ return { error: `${code} is not a valid WebSocket close code; codes below 1000 are not permitted on the wire` };
3105
+ }
3106
+ if (code <= 2999) {
3107
+ return { code, name: "Unassigned", description: "Reserved for future use by the WebSocket protocol specification. No meaning is defined yet.", range: "reserved-future", sentOnWire: true };
3108
+ }
3109
+ if (code <= 3999) {
3110
+ return { code, name: "Library/Framework Use", description: "Reserved for use by libraries, frameworks, and applications. Codes in this range are meant to be registered directly with IANA in the WebSocket Close Code Number Registry, so their meaning depends on which library or application defined them.", range: "library-framework", sentOnWire: true };
3111
+ }
3112
+ if (code <= 4999) {
3113
+ return { code, name: "Private Use", description: "Reserved for private use between a specific client and server implementation. These codes cannot be registered with IANA, so their meaning is defined entirely by the application using them.", range: "private-use", sentOnWire: true };
3114
+ }
3115
+ return { error: `${code} is outside the defined close code ranges (0-4999); codes 5000 and above have no assigned meaning` };
3116
+ }
3022
3117
 
3023
- export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, X_CONTENT_TYPE_OPTIONS_HEADER, analyzeEmailHeaders, assessContentTypeRisk, buildAcceptLanguage, buildCacheControl, buildClientHints, buildContentDisposition, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildPreferHeader, buildRangeHeader, buildReferrerPolicy, buildRetryAfterHeader, buildVaryHeader, buildXContentTypeOptionsHeader, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseAcceptLanguage, parseClientHintHeaders, parseContentDisposition, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parsePermissionsPolicy, parsePreferHeader, parseRetryAfterHeader, parseUserAgent, parseVaryHeader, parseXContentTypeOptionsHeader, searchHeaders, searchStatusCodes, splitSitemap, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
3118
+ export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, X_CONTENT_TYPE_OPTIONS_HEADER, analyzeEmailHeaders, assessContentTypeRisk, buildAcceptLanguage, buildCacheControl, buildClientHints, buildContentDisposition, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildPreferHeader, buildPriorityHeader, buildRangeHeader, buildReferrerPolicy, buildRetryAfterHeader, buildVaryHeader, buildXContentTypeOptionsHeader, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, listCloseCodes, lookupCloseCode, network_exports, parseAcceptLanguage, parseClientHintHeaders, parseContentDisposition, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parsePermissionsPolicy, parsePreferHeader, parsePriorityHeader, parseRetryAfterHeader, parseUserAgent, parseVaryHeader, parseXContentTypeOptionsHeader, searchHeaders, searchStatusCodes, splitSitemap, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
@@ -33,6 +33,7 @@ __export(units_exports, {
33
33
  calculateDepreciationSchedule: () => calculateDepreciationSchedule,
34
34
  calculateDiscount: () => calculateDiscount,
35
35
  calculateEmi: () => calculateEmi,
36
+ calculateExtraPaymentPayoff: () => calculateExtraPaymentPayoff,
36
37
  calculateFreelancerRate: () => calculateFreelancerRate,
37
38
  calculateGpa: () => calculateGpa,
38
39
  calculateLoan: () => calculateLoan,
@@ -1152,6 +1153,39 @@ function calculateLoan(principal, annualRatePercent, termMonths) {
1152
1153
  schedule
1153
1154
  };
1154
1155
  }
1156
+ function calculateExtraPaymentPayoff(principal, annualRatePercent, termMonths, extraMonthlyPayment) {
1157
+ const baseline = calculateLoan(principal, annualRatePercent, termMonths);
1158
+ if ("error" in baseline) return baseline;
1159
+ if (!Number.isFinite(extraMonthlyPayment) || extraMonthlyPayment < 0) {
1160
+ return { error: "Extra monthly payment must be zero or a positive number" };
1161
+ }
1162
+ const monthlyRate = annualRatePercent / 100 / 12;
1163
+ const payment = round2(baseline.monthlyPayment) + round2(extraMonthlyPayment);
1164
+ let balance = round2(principal);
1165
+ let totalInterestPaid = 0;
1166
+ let payoffMonths = termMonths;
1167
+ for (let month = 1; month <= termMonths; month++) {
1168
+ const interest = monthlyRate === 0 ? 0 : round2(balance * monthlyRate);
1169
+ let principalPortion = payment - interest;
1170
+ if (principalPortion > balance) principalPortion = balance;
1171
+ totalInterestPaid += interest;
1172
+ balance = round2(balance - principalPortion);
1173
+ if (balance <= 0) {
1174
+ payoffMonths = month;
1175
+ break;
1176
+ }
1177
+ }
1178
+ return {
1179
+ monthlyPayment: round2(baseline.monthlyPayment),
1180
+ extraMonthlyPayment: round2(extraMonthlyPayment),
1181
+ baselinePayoffMonths: termMonths,
1182
+ baselineTotalInterest: baseline.totalInterest,
1183
+ newPayoffMonths: payoffMonths,
1184
+ newTotalInterest: round2(totalInterestPaid),
1185
+ monthsSaved: termMonths - payoffMonths,
1186
+ interestSaved: round2(baseline.totalInterest - totalInterestPaid)
1187
+ };
1188
+ }
1155
1189
  var BARCODE_FORMAT_BY_LENGTH = { 8: "EAN-8", 12: "UPC-A", 13: "EAN-13" };
1156
1190
  function computeBarcodeCheckDigit(dataDigits) {
1157
1191
  let sum = 0;
@@ -2348,4 +2382,4 @@ function calculateFreelancerRate(input) {
2348
2382
  };
2349
2383
  }
2350
2384
 
2351
- export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, GPA_GRADE_POINTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateFreelancerRate, calculateGpa, calculateLoan, calculateMarginMarkup, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
2385
+ export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, GPA_GRADE_POINTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateMarginMarkup, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
package/dist/index.cjs CHANGED
@@ -8431,6 +8431,7 @@ __export(units_exports, {
8431
8431
  calculateDepreciationSchedule: () => calculateDepreciationSchedule,
8432
8432
  calculateDiscount: () => calculateDiscount,
8433
8433
  calculateEmi: () => calculateEmi,
8434
+ calculateExtraPaymentPayoff: () => calculateExtraPaymentPayoff,
8434
8435
  calculateFreelancerRate: () => calculateFreelancerRate,
8435
8436
  calculateGpa: () => calculateGpa,
8436
8437
  calculateLoan: () => calculateLoan,
@@ -9550,6 +9551,39 @@ function calculateLoan(principal, annualRatePercent, termMonths) {
9550
9551
  schedule
9551
9552
  };
9552
9553
  }
9554
+ function calculateExtraPaymentPayoff(principal, annualRatePercent, termMonths, extraMonthlyPayment) {
9555
+ const baseline = calculateLoan(principal, annualRatePercent, termMonths);
9556
+ if ("error" in baseline) return baseline;
9557
+ if (!Number.isFinite(extraMonthlyPayment) || extraMonthlyPayment < 0) {
9558
+ return { error: "Extra monthly payment must be zero or a positive number" };
9559
+ }
9560
+ const monthlyRate = annualRatePercent / 100 / 12;
9561
+ const payment = round2(baseline.monthlyPayment) + round2(extraMonthlyPayment);
9562
+ let balance = round2(principal);
9563
+ let totalInterestPaid = 0;
9564
+ let payoffMonths = termMonths;
9565
+ for (let month = 1; month <= termMonths; month++) {
9566
+ const interest = monthlyRate === 0 ? 0 : round2(balance * monthlyRate);
9567
+ let principalPortion = payment - interest;
9568
+ if (principalPortion > balance) principalPortion = balance;
9569
+ totalInterestPaid += interest;
9570
+ balance = round2(balance - principalPortion);
9571
+ if (balance <= 0) {
9572
+ payoffMonths = month;
9573
+ break;
9574
+ }
9575
+ }
9576
+ return {
9577
+ monthlyPayment: round2(baseline.monthlyPayment),
9578
+ extraMonthlyPayment: round2(extraMonthlyPayment),
9579
+ baselinePayoffMonths: termMonths,
9580
+ baselineTotalInterest: baseline.totalInterest,
9581
+ newPayoffMonths: payoffMonths,
9582
+ newTotalInterest: round2(totalInterestPaid),
9583
+ monthsSaved: termMonths - payoffMonths,
9584
+ interestSaved: round2(baseline.totalInterest - totalInterestPaid)
9585
+ };
9586
+ }
9553
9587
  var BARCODE_FORMAT_BY_LENGTH = { 8: "EAN-8", 12: "UPC-A", 13: "EAN-13" };
9554
9588
  function computeBarcodeCheckDigit(dataDigits) {
9555
9589
  let sum = 0;
@@ -10769,6 +10803,7 @@ __export(network_exports, {
10769
10803
  buildMapUrl: () => buildMapUrl,
10770
10804
  buildPermissionsPolicy: () => buildPermissionsPolicy,
10771
10805
  buildPreferHeader: () => buildPreferHeader,
10806
+ buildPriorityHeader: () => buildPriorityHeader,
10772
10807
  buildRangeHeader: () => buildRangeHeader,
10773
10808
  buildReferrerPolicy: () => buildReferrerPolicy,
10774
10809
  buildRetryAfterHeader: () => buildRetryAfterHeader,
@@ -10789,6 +10824,8 @@ __export(network_exports, {
10789
10824
  isValidIp: () => isValidIp2,
10790
10825
  isValidIpv4: () => isValidIpv42,
10791
10826
  isValidIpv6: () => isValidIpv6,
10827
+ listCloseCodes: () => listCloseCodes,
10828
+ lookupCloseCode: () => lookupCloseCode,
10792
10829
  parseAcceptLanguage: () => parseAcceptLanguage,
10793
10830
  parseClientHintHeaders: () => parseClientHintHeaders,
10794
10831
  parseContentDisposition: () => parseContentDisposition,
@@ -10799,6 +10836,7 @@ __export(network_exports, {
10799
10836
  parseIpv6Address: () => parseIpv6Address,
10800
10837
  parsePermissionsPolicy: () => parsePermissionsPolicy,
10801
10838
  parsePreferHeader: () => parsePreferHeader,
10839
+ parsePriorityHeader: () => parsePriorityHeader,
10802
10840
  parseRetryAfterHeader: () => parseRetryAfterHeader,
10803
10841
  parseUserAgent: () => parseUserAgent,
10804
10842
  parseVaryHeader: () => parseVaryHeader,
@@ -13765,6 +13803,97 @@ function parseRetryAfterHeader(raw) {
13765
13803
  const httpDate = parsed.toUTCString();
13766
13804
  return { header: `Retry-After: ${httpDate}`, headerValue: httpDate, type: "http-date", httpDate, iso: parsed.toISOString() };
13767
13805
  }
13806
+ function isValidUrgency(n) {
13807
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 7;
13808
+ }
13809
+ function buildPriorityHeader(input) {
13810
+ const { urgency, incremental } = input ?? {};
13811
+ if (urgency === void 0 && incremental === void 0) {
13812
+ return { error: "Provide at least one of urgency or incremental" };
13813
+ }
13814
+ if (urgency !== void 0 && !isValidUrgency(urgency)) {
13815
+ return { error: "urgency must be an integer between 0 and 7" };
13816
+ }
13817
+ const parts = [];
13818
+ if (urgency !== void 0) parts.push(`u=${urgency}`);
13819
+ if (incremental === true) parts.push("i");
13820
+ else if (incremental === false) parts.push("i=?0");
13821
+ const headerValue = parts.join(", ");
13822
+ return {
13823
+ header: `Priority: ${headerValue}`,
13824
+ headerValue,
13825
+ urgency: urgency ?? 3,
13826
+ incremental: incremental ?? false
13827
+ };
13828
+ }
13829
+ function parsePriorityHeader(raw) {
13830
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
13831
+ const withoutLabel = raw.trim().replace(/^priority:\s*/i, "");
13832
+ const parts = withoutLabel.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
13833
+ if (parts.length === 0) return { error: "Input is empty" };
13834
+ let urgency = 3;
13835
+ let incremental = false;
13836
+ for (const part of parts) {
13837
+ const eq = part.indexOf("=");
13838
+ const key = (eq === -1 ? part : part.slice(0, eq)).trim();
13839
+ const rawValue = eq === -1 ? void 0 : part.slice(eq + 1).trim();
13840
+ if (key === "u") {
13841
+ if (rawValue === void 0 || !/^\d+$/.test(rawValue)) return { error: `Invalid urgency value in "${part}"` };
13842
+ const n = Number(rawValue);
13843
+ if (n < 0 || n > 7) return { error: `urgency must be between 0 and 7, got ${n}` };
13844
+ urgency = n;
13845
+ } else if (key === "i") {
13846
+ if (rawValue === void 0 || rawValue === "?1") incremental = true;
13847
+ else if (rawValue === "?0") incremental = false;
13848
+ else return { error: `Invalid incremental value in "${part}"` };
13849
+ }
13850
+ }
13851
+ const headerValue = incremental ? `u=${urgency}, i` : `u=${urgency}, i=?0`;
13852
+ return { header: `Priority: ${headerValue}`, headerValue, urgency, incremental };
13853
+ }
13854
+ var WELL_KNOWN_CLOSE_CODES = {
13855
+ 1e3: { name: "Normal Closure", description: "The connection successfully completed the purpose for which it was created.", sentOnWire: true },
13856
+ 1001: { name: "Going Away", description: "An endpoint is going away, such as a server shutting down or a browser navigating away from the page.", sentOnWire: true },
13857
+ 1002: { name: "Protocol Error", description: "An endpoint is terminating the connection due to a protocol error.", sentOnWire: true },
13858
+ 1003: { name: "Unsupported Data", description: "An endpoint received a data type it cannot accept, such as a binary-only endpoint receiving a text message.", sentOnWire: true },
13859
+ 1004: { name: "Reserved", description: "Reserved. A meaning might be defined in the future.", sentOnWire: true },
13860
+ 1005: { name: "No Status Received", description: "A reserved value used internally by libraries to indicate no status code was actually present in the close frame. Must not be sent on the wire.", sentOnWire: false },
13861
+ 1006: { name: "Abnormal Closure", description: "A reserved value used internally by libraries to indicate the connection closed without a close frame, e.g. the TCP connection dropped. Must not be sent on the wire.", sentOnWire: false },
13862
+ 1007: { name: "Invalid frame payload data", description: "An endpoint is terminating the connection because it received data inconsistent with the message type, such as non-UTF-8 data in a text message.", sentOnWire: true },
13863
+ 1008: { name: "Policy Violation", description: "A generic status code used when no other code fits and the endpoint does not want to reveal further details, or a message violated its policy.", sentOnWire: true },
13864
+ 1009: { name: "Message Too Big", description: "An endpoint is terminating the connection because it received a message too large to process.", sentOnWire: true },
13865
+ 1010: { name: "Mandatory Extension", description: "A client is terminating the connection because the server did not negotiate one or more extensions the client required.", sentOnWire: true },
13866
+ 1011: { name: "Internal Error", description: "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.", sentOnWire: true },
13867
+ 1012: { name: "Service Restart", description: "The server is terminating the connection because it is restarting.", sentOnWire: true },
13868
+ 1013: { name: "Try Again Later", description: "The server is terminating the connection due to a temporary condition, such as being overloaded, and the client should reconnect later.", sentOnWire: true },
13869
+ 1014: { name: "Bad Gateway", description: "The server, acting as a gateway or proxy, received an invalid response from an upstream server it connected to while processing the request.", sentOnWire: true },
13870
+ 1015: { name: "TLS Handshake", description: "A reserved value used internally by libraries to indicate the TLS handshake failed. Must not be sent on the wire.", sentOnWire: false }
13871
+ };
13872
+ function listCloseCodes() {
13873
+ return Object.entries(WELL_KNOWN_CLOSE_CODES).map(([code, info]) => ({ code: Number(code), name: info.name, description: info.description, range: "standard", sentOnWire: info.sentOnWire })).sort((a, b) => a.code - b.code);
13874
+ }
13875
+ function lookupCloseCode(code) {
13876
+ if (typeof code !== "number" || !Number.isInteger(code) || code < 0 || code > 65535) {
13877
+ return { error: "code must be an integer between 0 and 65535" };
13878
+ }
13879
+ const known = WELL_KNOWN_CLOSE_CODES[code];
13880
+ if (known) {
13881
+ return { code, name: known.name, description: known.description, range: "standard", sentOnWire: known.sentOnWire };
13882
+ }
13883
+ if (code < 1e3) {
13884
+ return { error: `${code} is not a valid WebSocket close code; codes below 1000 are not permitted on the wire` };
13885
+ }
13886
+ if (code <= 2999) {
13887
+ return { code, name: "Unassigned", description: "Reserved for future use by the WebSocket protocol specification. No meaning is defined yet.", range: "reserved-future", sentOnWire: true };
13888
+ }
13889
+ if (code <= 3999) {
13890
+ return { code, name: "Library/Framework Use", description: "Reserved for use by libraries, frameworks, and applications. Codes in this range are meant to be registered directly with IANA in the WebSocket Close Code Number Registry, so their meaning depends on which library or application defined them.", range: "library-framework", sentOnWire: true };
13891
+ }
13892
+ if (code <= 4999) {
13893
+ return { code, name: "Private Use", description: "Reserved for private use between a specific client and server implementation. These codes cannot be registered with IANA, so their meaning is defined entirely by the application using them.", range: "private-use", sentOnWire: true };
13894
+ }
13895
+ return { error: `${code} is outside the defined close code ranges (0-4999); codes 5000 and above have no assigned meaning` };
13896
+ }
13768
13897
 
13769
13898
  // src/tools/api.ts
13770
13899
  var api_exports = {};
package/dist/index.d.cts CHANGED
@@ -5,8 +5,8 @@ export { t as text } from './text-DWUcY8H1.cjs';
5
5
  export { d as data } from './data-vxXrBlWh.cjs';
6
6
  export { g as generators } from './generators-DAXXrMvG.cjs';
7
7
  export { t as time } from './time-KqJjdqII.cjs';
8
- export { u as units } from './units-ClKR6Jxz.cjs';
9
- export { n as network } from './network-CXt69chQ.cjs';
8
+ export { u as units } from './units-izyUpUAb.cjs';
9
+ export { n as network } from './network-DewFJy4E.cjs';
10
10
  export { a as api } from './api-DmTNE4c1.cjs';
11
11
  export { c as code } from './code-rxidGgvB.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-DWUcY8H1.js';
5
5
  export { d as data } from './data-vxXrBlWh.js';
6
6
  export { g as generators } from './generators-DAXXrMvG.js';
7
7
  export { t as time } from './time-KqJjdqII.js';
8
- export { u as units } from './units-ClKR6Jxz.js';
9
- export { n as network } from './network-CXt69chQ.js';
8
+ export { u as units } from './units-izyUpUAb.js';
9
+ export { n as network } from './network-DewFJy4E.js';
10
10
  export { a as api } from './api-DmTNE4c1.js';
11
11
  export { c as code } from './code-rxidGgvB.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-226EH3ZI.js';
2
- export { units_exports as units } from './chunk-B7366PQJ.js';
3
- export { network_exports as network } from './chunk-2WT347WO.js';
2
+ export { units_exports as units } from './chunk-43WFN3CW.js';
3
+ export { network_exports as network } from './chunk-43L7H3L2.js';
4
4
  export { api_exports as api } from './chunk-52ZDKSIX.js';
5
5
  export { code_exports as code } from './chunk-JW4NG2GN.js';
6
6
  export { color_exports as color } from './chunk-BPVAB4P2.js';