@utilix-tech/sdk 0.43.0 → 0.45.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,14 @@
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.45.0
6
+ - Add `calculateCdMaturity` (cd-maturity-calculator): compute a certificate of deposit's maturity value and effective annual yield (APY) from a principal, nominal rate, term, and compounding frequency, plus the net payout if withdrawn early.
7
+ - Add `buildClientHints`, `parseClientHintHeaders` (client-hints-header-builder): build an `Accept-CH`/`Critical-CH` response header from a list of client hint names with a plain-English note per hint, or decode raw Client Hints request-header text into structured values.
8
+
9
+ ## 0.44.0
10
+ - Add `buildPermissionsPolicy`, `parsePermissionsPolicy` (permissions-policy-builder): build a `Permissions-Policy` response header from browser features and their per-feature allowlist with a plain-English note per feature, or parse an existing one into the same shape.
11
+ - Add `calculatePaybackPeriod` (payback-period-calculator): compute the simple and discounted payback period from an initial investment and a series of periodic cash flows.
12
+
5
13
  ## 0.43.0
6
14
  - Add `validateManifest` (manifest-validator): validate a web app manifest.json against the W3C Web App Manifest spec's installability requirements, including a 512x512 icon and a maskable icon check.
7
15
  - Add `lintToolSchema` (tool-schema-linter): lint an LLM tool/function-calling definition (OpenAI, Anthropic, or MCP format) for missing descriptions, invalid names, empty enums, and required/properties mismatches.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **622 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **630 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)
@@ -312,7 +312,7 @@ formatIso8601Duration(90061); // { iso: "P1DT1H1M1S", ... }
312
312
  ### `/units`: Conversions
313
313
 
314
314
  ```ts
315
- 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 } from "@utilix-tech/sdk/units";
315
+ 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 } from "@utilix-tech/sdk/units";
316
316
 
317
317
  // File sizes
318
318
  convertBytes(1073741824, "GB"); // 1
@@ -422,6 +422,14 @@ calculateCashBreakdown(53.75, USD_DENOMINATIONS);
422
422
  // Change owed for a payment/tender pair, broken down the same way
423
423
  calculateChange(4.65, 5.0, USD_DENOMINATIONS);
424
424
  // { changeDue: 0.35, breakdown: [{ denomination: 0.25, count: 1, subtotal: 0.25 }, ...], totalCount: 2 }
425
+
426
+ // Payback period: simple + discounted, from an initial investment and periodic cash flows
427
+ calculatePaybackPeriod({ initialInvestment: 1000, cashFlows: [400, 400, 400], discountRatePercent: 8 });
428
+ // { paybackPeriod: 2.5, paybackPeriodRounded: 3, recovered: true, discountedPaybackPeriod: 2.9029, ... }
429
+
430
+ // Certificate of deposit maturity value, APY, and net early-withdrawal payout
431
+ calculateCdMaturity(1000, 12, 12, 12, 3);
432
+ // { maturityValue: 1126.83, totalInterest: 126.83, apy: 12.6825, earlyWithdrawalPayout: 1096.83, ... }
425
433
  ```
426
434
 
427
435
  ---
@@ -433,7 +441,8 @@ import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
433
441
  searchStatusCodes, buildGeoUrl, countryCodeToFlag,
434
442
  parseHar, validateSitemap, validateRobotsTxt,
435
443
  parseUserAgent, buildCacheControl, buildReferrerPolicy, parseIpv6Address, buildLinkHeader, checkSetCookieHeader, analyzeEmailHeaders,
436
- buildRangeHeader, parseContentRangeHeader, buildVaryHeader, parseVaryHeader, validateManifest } from "@utilix-tech/sdk/network";
444
+ buildRangeHeader, parseContentRangeHeader, buildVaryHeader, parseVaryHeader, validateManifest,
445
+ buildPermissionsPolicy, parsePermissionsPolicy, buildClientHints, parseClientHintHeaders } from "@utilix-tech/sdk/network";
437
446
 
438
447
  getStatusCode(404);
439
448
  // { code: 404, text: "Not Found", description: "...", category: "Client Error" }
@@ -508,6 +517,22 @@ parseVaryHeader("Vary: Accept-Encoding, Accept-Language");
508
517
  // Validate a web app manifest.json against the W3C spec's installability requirements
509
518
  validateManifest(JSON.stringify({ name: "My App", start_url: "/", display: "standalone", icons: [{ src: "/icon-512.png", sizes: "512x512", purpose: "any maskable" }] }));
510
519
  // { issues: [...], valid: true, summary: { hasMaskableIcon: true, has512Icon: true, ... } }
520
+
521
+ // Build a Permissions-Policy header from features and their allowlists, with a plain-English note per feature
522
+ buildPermissionsPolicy([{ feature: "camera", allowlist: [] }, { feature: "geolocation", allowlist: ["self"] }]);
523
+ // { header: "Permissions-Policy: camera=(), geolocation=(self)", directives: [...], warnings: [] }
524
+
525
+ // Parse an existing Permissions-Policy header value into the same shape
526
+ parsePermissionsPolicy("camera=(), geolocation=(self)");
527
+ // { header: "Permissions-Policy: camera=(), geolocation=(self)", directives: [...], warnings: [] }
528
+
529
+ // Build an Accept-CH header from a list of client hint names, with a plain-English note per hint
530
+ buildClientHints(["Sec-CH-UA-Platform", "Sec-CH-DPR"]);
531
+ // { acceptCH: "Sec-CH-UA-Platform, Sec-CH-DPR", hints: [...] }
532
+
533
+ // Decode raw Client Hints request-header text into structured values
534
+ parseClientHintHeaders("Sec-CH-UA-Mobile: ?0");
535
+ // { hints: [{ name: "Sec-CH-UA-Mobile", value: false, ... }], unrecognized: [] }
511
536
  ```
512
537
 
513
538
  ---
@@ -745,8 +770,8 @@ readVideoInfo(new Uint8Array(videoBytes));
745
770
  | `@utilix-tech/sdk/data` | CSV, YAML, TOML, XML, INI, NDJSON parse and convert, SRT/WebVTT subtitles, Markdown front matter parser |
746
771
  | `@utilix-tech/sdk/generators` | UUID v4/v7, ULID, passwords, strength check, entropy estimator, fake data |
747
772
  | `@utilix-tech/sdk/time` | Date diff, timezone convert, cron parse, relative time |
748
- | `@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 |
749
- | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing, IPv6 address compress/expand |
773
+ | `@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 |
774
+ | `@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 |
750
775
  | `@utilix-tech/sdk/api` | cURL build/parse, JWT decode/sign, JWKS parse, CORS headers, CSP header |
751
776
  | `@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 |
752
777
  | `@utilix-tech/sdk/color` | Hex/RGB/HSL/HSV/CMYK convert, palette generation, contrast check |
@@ -3,17 +3,21 @@ import { __export } from './chunk-MLKGABMK.js';
3
3
  // src/tools/network.ts
4
4
  var network_exports = {};
5
5
  __export(network_exports, {
6
+ CLIENT_HINTS: () => CLIENT_HINTS,
6
7
  DNS_RECORD_TYPES: () => DNS_RECORD_TYPES,
7
8
  HTTP_HEADERS: () => HTTP_HEADERS,
8
9
  LINK_REL_VALUES: () => LINK_REL_VALUES,
10
+ PERMISSIONS_POLICY_FEATURES: () => PERMISSIONS_POLICY_FEATURES,
9
11
  REFERRER_POLICY_VALUES: () => REFERRER_POLICY_VALUES,
10
12
  STATUS_CODES: () => STATUS_CODES,
11
13
  analyzeEmailHeaders: () => analyzeEmailHeaders,
12
14
  buildCacheControl: () => buildCacheControl,
15
+ buildClientHints: () => buildClientHints,
13
16
  buildDnsQueryUrl: () => buildDnsQueryUrl,
14
17
  buildGeoUrl: () => buildGeoUrl,
15
18
  buildLinkHeader: () => buildLinkHeader,
16
19
  buildMapUrl: () => buildMapUrl,
20
+ buildPermissionsPolicy: () => buildPermissionsPolicy,
17
21
  buildRangeHeader: () => buildRangeHeader,
18
22
  buildReferrerPolicy: () => buildReferrerPolicy,
19
23
  buildVaryHeader: () => buildVaryHeader,
@@ -32,11 +36,13 @@ __export(network_exports, {
32
36
  isValidIp: () => isValidIp,
33
37
  isValidIpv4: () => isValidIpv4,
34
38
  isValidIpv6: () => isValidIpv6,
39
+ parseClientHintHeaders: () => parseClientHintHeaders,
35
40
  parseContentRangeHeader: () => parseContentRangeHeader,
36
41
  parseDnsResponse: () => parseDnsResponse,
37
42
  parseGeoResponse: () => parseGeoResponse,
38
43
  parseHar: () => parseHar,
39
44
  parseIpv6Address: () => parseIpv6Address,
45
+ parsePermissionsPolicy: () => parsePermissionsPolicy,
40
46
  parseUserAgent: () => parseUserAgent,
41
47
  parseVaryHeader: () => parseVaryHeader,
42
48
  searchHeaders: () => searchHeaders,
@@ -2471,5 +2477,206 @@ function validateManifest(jsonText) {
2471
2477
  }
2472
2478
  };
2473
2479
  }
2480
+ var PERMISSIONS_POLICY_NOTES = {
2481
+ camera: "Controls access to video capture devices (getUserMedia video).",
2482
+ microphone: "Controls access to audio capture devices (getUserMedia audio).",
2483
+ geolocation: "Controls access to the Geolocation API.",
2484
+ autoplay: "Controls whether media elements can autoplay without a user gesture.",
2485
+ fullscreen: "Controls access to the Fullscreen API (requestFullscreen).",
2486
+ payment: "Controls access to the Payment Request API.",
2487
+ usb: "Controls access to the WebUSB API.",
2488
+ accelerometer: "Controls access to the Accelerometer sensor API.",
2489
+ gyroscope: "Controls access to the Gyroscope sensor API.",
2490
+ magnetometer: "Controls access to the Magnetometer sensor API.",
2491
+ midi: "Controls access to the Web MIDI API.",
2492
+ "picture-in-picture": "Controls whether video elements can enter picture-in-picture mode.",
2493
+ "sync-xhr": "Controls whether synchronous XMLHttpRequest is allowed.",
2494
+ "display-capture": "Controls access to screen/tab capture (getDisplayMedia).",
2495
+ "encrypted-media": "Controls access to the Encrypted Media Extensions API (DRM playback).",
2496
+ "clipboard-read": "Controls read access to the async Clipboard API.",
2497
+ "clipboard-write": "Controls write access to the async Clipboard API.",
2498
+ "screen-wake-lock": "Controls access to the Screen Wake Lock API.",
2499
+ "web-share": "Controls access to the Web Share API.",
2500
+ "xr-spatial-tracking": "Controls access to WebXR device sensors and spatial tracking."
2501
+ };
2502
+ var PERMISSIONS_POLICY_FEATURES = Object.keys(PERMISSIONS_POLICY_NOTES);
2503
+ function _formatPermissionsPolicyAllowlist(allowlist) {
2504
+ if (allowlist.includes("*")) return "*";
2505
+ if (allowlist.length === 0) return "()";
2506
+ return "(" + allowlist.map((o) => o === "self" ? "self" : `"${o}"`).join(" ") + ")";
2507
+ }
2508
+ function buildPermissionsPolicy(features) {
2509
+ if (!Array.isArray(features) || features.length === 0) return { error: "At least one feature is required" };
2510
+ const warnings = [];
2511
+ const seen = /* @__PURE__ */ new Map();
2512
+ for (const f of features) {
2513
+ if (!f || typeof f.feature !== "string" || f.feature.trim() === "") continue;
2514
+ const name = f.feature.trim().toLowerCase();
2515
+ const allowlist = Array.isArray(f.allowlist) ? f.allowlist.map((o) => typeof o === "string" ? o.trim() : "").filter((o) => o.length > 0) : [];
2516
+ if (seen.has(name)) warnings.push(`duplicate feature "${name}" was merged, the last occurrence wins`);
2517
+ seen.set(name, { feature: name, allowlist });
2518
+ }
2519
+ if (seen.size === 0) return { error: "At least one feature is required" };
2520
+ const directives = [];
2521
+ for (const { feature, allowlist } of seen.values()) {
2522
+ if (!PERMISSIONS_POLICY_NOTES[feature]) {
2523
+ warnings.push(`"${feature}" is not a widely recognized feature name, it will still be included as written`);
2524
+ }
2525
+ if (allowlist.includes("*") && allowlist.length > 1) {
2526
+ warnings.push(`"${feature}" combines "*" with other origins; "*" already allows every origin so the others are redundant`);
2527
+ }
2528
+ const invalidOrigin = allowlist.find((o) => o !== "self" && o !== "*" && !/^https?:\/\//i.test(o));
2529
+ if (invalidOrigin) {
2530
+ warnings.push(`"${invalidOrigin}" on "${feature}" doesn't look like an origin (expected "self", "*", or an http(s):// URL); it will still be quoted as written`);
2531
+ }
2532
+ directives.push({
2533
+ feature,
2534
+ value: _formatPermissionsPolicyAllowlist(allowlist),
2535
+ note: PERMISSIONS_POLICY_NOTES[feature] ?? `Controls access to the "${feature}" feature.`
2536
+ });
2537
+ }
2538
+ const headerValue = directives.map((d) => `${d.feature}=${d.value}`).join(", ");
2539
+ return { header: `Permissions-Policy: ${headerValue}`, headerValue, directives, warnings };
2540
+ }
2541
+ function _splitPermissionsPolicyTopLevel(input) {
2542
+ const segments = [];
2543
+ let depth = 0;
2544
+ let current = "";
2545
+ for (const ch of input) {
2546
+ if (ch === "(") depth++;
2547
+ if (ch === ")") depth--;
2548
+ if (ch === "," && depth === 0) {
2549
+ if (current.trim()) segments.push(current.trim());
2550
+ current = "";
2551
+ } else {
2552
+ current += ch;
2553
+ }
2554
+ }
2555
+ if (current.trim()) segments.push(current.trim());
2556
+ return segments;
2557
+ }
2558
+ function _parsePermissionsPolicyAllowlist(rawValue) {
2559
+ if (rawValue === "*") return ["*"];
2560
+ if (rawValue === "()" || rawValue === "") return [];
2561
+ const inner = rawValue.replace(/^\(/, "").replace(/\)$/, "");
2562
+ const tokens = inner.match(/"[^"]*"|\S+/g) ?? [];
2563
+ return tokens.map((t) => t.replace(/^"|"$/g, ""));
2564
+ }
2565
+ function parsePermissionsPolicy(raw) {
2566
+ if (!raw || raw.trim() === "") return { error: "Input is empty" };
2567
+ const withoutLabel = raw.trim().replace(/^permissions-policy:\s*/i, "");
2568
+ const segments = _splitPermissionsPolicyTopLevel(withoutLabel);
2569
+ if (segments.length === 0) return { error: "No directives found" };
2570
+ const features = [];
2571
+ for (const segment of segments) {
2572
+ const eq = segment.indexOf("=");
2573
+ if (eq === -1) continue;
2574
+ const feature = segment.slice(0, eq).trim();
2575
+ const rawValue = segment.slice(eq + 1).trim();
2576
+ features.push({ feature, allowlist: _parsePermissionsPolicyAllowlist(rawValue) });
2577
+ }
2578
+ if (features.length === 0) return { error: "No directives found" };
2579
+ return buildPermissionsPolicy(features);
2580
+ }
2581
+ var CLIENT_HINTS = [
2582
+ { name: "Sec-CH-UA", description: "The browser's brand(s) and significant version, sent automatically on every request without an Accept-CH opt-in.", lowEntropy: true, valueType: "sf-list-brand" },
2583
+ { name: "Sec-CH-UA-Mobile", description: "Whether the browser is running on a mobile device (?1) or not (?0).", lowEntropy: true, valueType: "boolean" },
2584
+ { name: "Sec-CH-UA-Platform", description: 'The platform/OS the browser is running on, e.g. "Windows" or "Android".', lowEntropy: true, valueType: "string" },
2585
+ { name: "Sec-CH-UA-Full-Version-List", description: "The full version number for each brand in the user agent, not just the significant version.", lowEntropy: false, valueType: "sf-list-brand" },
2586
+ { name: "Sec-CH-UA-Platform-Version", description: 'The exact OS version, e.g. "15.0.0".', lowEntropy: false, valueType: "string" },
2587
+ { name: "Sec-CH-UA-Arch", description: 'The underlying CPU architecture, e.g. "x86" or "arm".', lowEntropy: false, valueType: "string" },
2588
+ { name: "Sec-CH-UA-Bitness", description: 'The CPU architecture bitness, e.g. "64".', lowEntropy: false, valueType: "string" },
2589
+ { name: "Sec-CH-UA-Model", description: "The device model, mostly populated on mobile devices.", lowEntropy: false, valueType: "string" },
2590
+ { name: "Sec-CH-UA-WoW64", description: "Whether a 32-bit browser is running on 64-bit Windows.", lowEntropy: false, valueType: "boolean" },
2591
+ { name: "Sec-CH-UA-Form-Factor", description: `The device's form factor, e.g. "Desktop", "Mobile", "Tablet", or "XR".`, lowEntropy: false, valueType: "string" },
2592
+ { name: "Sec-CH-Prefers-Color-Scheme", description: "The user's preferred color scheme, mirrors the prefers-color-scheme media feature.", lowEntropy: false, valueType: "string" },
2593
+ { name: "Sec-CH-Prefers-Reduced-Motion", description: "Whether the user has requested reduced motion.", lowEntropy: false, valueType: "string" },
2594
+ { name: "Sec-CH-Prefers-Reduced-Transparency", description: "Whether the user has requested reduced transparency.", lowEntropy: false, valueType: "string" },
2595
+ { name: "Sec-CH-Viewport-Width", description: "The layout viewport width in CSS pixels.", lowEntropy: false, valueType: "number" },
2596
+ { name: "Sec-CH-Viewport-Height", description: "The layout viewport height in CSS pixels.", lowEntropy: false, valueType: "number" },
2597
+ { name: "Sec-CH-DPR", description: "The device pixel ratio.", lowEntropy: false, valueType: "number" },
2598
+ { name: "Sec-CH-Width", description: "The intrinsic width of a requested image resource, set per-request via the image's `sizes` attribute.", lowEntropy: false, valueType: "number" },
2599
+ { name: "Sec-CH-Device-Memory", description: "Approximate device RAM in GiB, rounded to the nearest power of two for privacy.", lowEntropy: false, valueType: "number" },
2600
+ { name: "Save-Data", description: "Legacy hint signaling the user opted into a reduced-data mode; sent automatically like a low-entropy hint.", lowEntropy: true, valueType: "boolean" }
2601
+ ];
2602
+ function _findClientHint(raw) {
2603
+ const normalized = raw.trim().toLowerCase();
2604
+ return CLIENT_HINTS.find((d) => d.name.toLowerCase() === normalized);
2605
+ }
2606
+ function buildClientHints(hintNames, criticalHintNames = []) {
2607
+ if (!Array.isArray(hintNames) || hintNames.length === 0) return { error: "At least one client hint is required" };
2608
+ const resolved = [];
2609
+ for (const raw of hintNames) {
2610
+ if (typeof raw !== "string" || raw.trim() === "") continue;
2611
+ const def = _findClientHint(raw);
2612
+ if (!def) return { error: `Unknown client hint: "${raw}"` };
2613
+ resolved.push(def);
2614
+ }
2615
+ if (resolved.length === 0) return { error: "At least one client hint is required" };
2616
+ const critical = [];
2617
+ if (Array.isArray(criticalHintNames)) {
2618
+ for (const raw of criticalHintNames) {
2619
+ if (typeof raw !== "string" || raw.trim() === "") continue;
2620
+ const def = _findClientHint(raw);
2621
+ if (!def) return { error: `Unknown client hint: "${raw}"` };
2622
+ if (!resolved.some((d) => d.name === def.name)) {
2623
+ return { error: `Critical hint "${def.name}" must also be included in hintNames` };
2624
+ }
2625
+ critical.push(def);
2626
+ }
2627
+ }
2628
+ const uniqueNames = Array.from(new Set(resolved.map((d) => d.name)));
2629
+ const result = {
2630
+ acceptCH: uniqueNames.join(", "),
2631
+ hints: resolved.map((d) => ({ name: d.name, description: d.description, lowEntropy: d.lowEntropy }))
2632
+ };
2633
+ if (critical.length > 0) {
2634
+ result.criticalCH = Array.from(new Set(critical.map((d) => d.name))).join(", ");
2635
+ }
2636
+ return result;
2637
+ }
2638
+ function _decodeClientHintValue(valueType, rawValue) {
2639
+ switch (valueType) {
2640
+ case "boolean":
2641
+ if (rawValue === "?1") return true;
2642
+ if (rawValue === "?0") return false;
2643
+ return rawValue;
2644
+ case "string":
2645
+ return rawValue.replace(/^"|"$/g, "");
2646
+ case "number": {
2647
+ const n = Number(rawValue);
2648
+ return Number.isFinite(n) ? n : rawValue;
2649
+ }
2650
+ case "sf-list-brand":
2651
+ return rawValue.split(",").map((part) => {
2652
+ const m = part.trim().match(/^"([^"]*)";v="([^"]*)"$/);
2653
+ return m ? { brand: m[1], version: m[2] } : { raw: part.trim() };
2654
+ });
2655
+ default:
2656
+ return rawValue;
2657
+ }
2658
+ }
2659
+ function parseClientHintHeaders(rawHeaders) {
2660
+ if (typeof rawHeaders !== "string" || rawHeaders.trim() === "") return { error: "Provide one or more header lines to parse" };
2661
+ const lines = rawHeaders.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2662
+ const hints = [];
2663
+ const unrecognized = [];
2664
+ for (const line of lines) {
2665
+ const idx = line.indexOf(":");
2666
+ if (idx === -1) continue;
2667
+ const name = line.slice(0, idx).trim();
2668
+ const rawValue = line.slice(idx + 1).trim();
2669
+ const def = _findClientHint(name);
2670
+ if (!def) {
2671
+ unrecognized.push({ name, rawValue });
2672
+ continue;
2673
+ }
2674
+ hints.push({ name: def.name, rawValue, description: def.description, value: _decodeClientHintValue(def.valueType, rawValue) });
2675
+ }
2676
+ if (hints.length === 0 && unrecognized.length === 0) {
2677
+ return { error: 'No parsable header lines found (expected "Header-Name: value" per line)' };
2678
+ }
2679
+ return { hints, unrecognized };
2680
+ }
2474
2681
 
2475
- export { DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, REFERRER_POLICY_VALUES, STATUS_CODES, analyzeEmailHeaders, buildCacheControl, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildRangeHeader, buildReferrerPolicy, buildVaryHeader, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parseUserAgent, parseVaryHeader, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
2682
+ export { CLIENT_HINTS, DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, PERMISSIONS_POLICY_FEATURES, REFERRER_POLICY_VALUES, STATUS_CODES, analyzeEmailHeaders, buildCacheControl, buildClientHints, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildPermissionsPolicy, buildRangeHeader, buildReferrerPolicy, buildVaryHeader, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseClientHintHeaders, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parsePermissionsPolicy, parseUserAgent, parseVaryHeader, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateManifest, validateRobotsTxt, validateSitemap };
@@ -22,12 +22,14 @@ __export(units_exports, {
22
22
  calcSubnet: () => calcSubnet,
23
23
  calculateBreakEven: () => calculateBreakEven,
24
24
  calculateCashBreakdown: () => calculateCashBreakdown,
25
+ calculateCdMaturity: () => calculateCdMaturity,
25
26
  calculateChange: () => calculateChange,
26
27
  calculateCompoundInterest: () => calculateCompoundInterest,
27
28
  calculateDiscount: () => calculateDiscount,
28
29
  calculateEmi: () => calculateEmi,
29
30
  calculateLoan: () => calculateLoan,
30
31
  calculateMarginMarkup: () => calculateMarginMarkup,
32
+ calculatePaybackPeriod: () => calculatePaybackPeriod,
31
33
  calculatePercentageChange: () => calculatePercentageChange,
32
34
  calculateRoi: () => calculateRoi,
33
35
  calculateRuleOf72: () => calculateRuleOf72,
@@ -1928,5 +1930,97 @@ function calculateChange(amountDue, amountTendered, denominations) {
1928
1930
  remainder: breakdown.remainder
1929
1931
  };
1930
1932
  }
1933
+ function _computePayback(initialInvestment, flows) {
1934
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
1935
+ let cumulative = 0;
1936
+ const cumulativeCashFlows = [];
1937
+ let period = null;
1938
+ for (let i = 0; i < flows.length; i++) {
1939
+ const prevCumulative = cumulative;
1940
+ cumulative += flows[i];
1941
+ cumulativeCashFlows.push(round4(cumulative));
1942
+ if (period === null && cumulative >= initialInvestment) {
1943
+ const remaining = initialInvestment - prevCumulative;
1944
+ const fraction = flows[i] !== 0 ? remaining / flows[i] : 0;
1945
+ period = i + fraction;
1946
+ }
1947
+ }
1948
+ return { cumulative: cumulativeCashFlows, period };
1949
+ }
1950
+ function calculatePaybackPeriod(input) {
1951
+ const { initialInvestment, cashFlows, discountRatePercent } = input;
1952
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
1953
+ if (!Number.isFinite(initialInvestment) || initialInvestment <= 0) {
1954
+ return { error: "initialInvestment must be a finite number greater than zero" };
1955
+ }
1956
+ if (!Array.isArray(cashFlows) || cashFlows.length === 0) {
1957
+ return { error: "cashFlows must be a non-empty array of numbers" };
1958
+ }
1959
+ if (cashFlows.some((f) => !Number.isFinite(f))) {
1960
+ return { error: "Every cash flow must be a finite number" };
1961
+ }
1962
+ const { cumulative, period } = _computePayback(initialInvestment, cashFlows);
1963
+ const result = {
1964
+ initialInvestment: round4(initialInvestment),
1965
+ cashFlows: cashFlows.map(round4),
1966
+ cumulativeCashFlows: cumulative,
1967
+ paybackPeriod: period !== null ? round4(period) : null,
1968
+ paybackPeriodRounded: period !== null ? Math.ceil(period) : null,
1969
+ recovered: period !== null
1970
+ };
1971
+ if (discountRatePercent !== void 0) {
1972
+ if (!Number.isFinite(discountRatePercent) || discountRatePercent < 0) {
1973
+ return { error: "discountRatePercent must be a finite, non-negative number when provided" };
1974
+ }
1975
+ const r = discountRatePercent / 100;
1976
+ const discountedCashFlows = cashFlows.map((f, i) => f / Math.pow(1 + r, i + 1));
1977
+ const { cumulative: discountedCumulative, period: discountedPeriod } = _computePayback(initialInvestment, discountedCashFlows);
1978
+ result.discountRatePercent = round4(discountRatePercent);
1979
+ result.discountedCashFlows = discountedCashFlows.map(round4);
1980
+ result.discountedCumulativeCashFlows = discountedCumulative;
1981
+ result.discountedPaybackPeriod = discountedPeriod !== null ? round4(discountedPeriod) : null;
1982
+ result.discountedPaybackPeriodRounded = discountedPeriod !== null ? Math.ceil(discountedPeriod) : null;
1983
+ result.discountedRecovered = discountedPeriod !== null;
1984
+ }
1985
+ return result;
1986
+ }
1987
+ function calculateCdMaturity(principal, annualRatePercent, termMonths, compoundsPerYear, earlyWithdrawalPenaltyMonths) {
1988
+ if (!Number.isFinite(principal) || principal <= 0) return { error: "Principal must be a finite number greater than zero" };
1989
+ if (!Number.isFinite(annualRatePercent) || annualRatePercent < 0) {
1990
+ return { error: "Annual interest rate must be zero or a positive number" };
1991
+ }
1992
+ if (!Number.isFinite(termMonths) || termMonths <= 0) return { error: "Term must be a positive number of months" };
1993
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
1994
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
1995
+ }
1996
+ if (earlyWithdrawalPenaltyMonths !== void 0 && (!Number.isFinite(earlyWithdrawalPenaltyMonths) || earlyWithdrawalPenaltyMonths < 0)) {
1997
+ return { error: "earlyWithdrawalPenaltyMonths must be zero or a positive number when provided" };
1998
+ }
1999
+ const round22 = (n) => Math.round(n * 100) / 100;
2000
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
2001
+ const ratePerPeriod = annualRatePercent / 100 / compoundsPerYear;
2002
+ const totalPeriods = compoundsPerYear * (termMonths / 12);
2003
+ const growth = ratePerPeriod === 0 ? 1 : Math.pow(1 + ratePerPeriod, totalPeriods);
2004
+ const maturityValue = principal * growth;
2005
+ const totalInterest = maturityValue - principal;
2006
+ const apy = ratePerPeriod === 0 ? 0 : (Math.pow(1 + ratePerPeriod, compoundsPerYear) - 1) * 100;
2007
+ const result = {
2008
+ principal: round22(principal),
2009
+ annualRatePercent: round4(annualRatePercent),
2010
+ termMonths,
2011
+ compoundsPerYear,
2012
+ maturityValue: round22(maturityValue),
2013
+ totalInterest: round22(totalInterest),
2014
+ apy: round4(apy)
2015
+ };
2016
+ if (earlyWithdrawalPenaltyMonths !== void 0) {
2017
+ const monthlyRate = annualRatePercent / 100 / 12;
2018
+ const forfeited = Math.min(principal * monthlyRate * earlyWithdrawalPenaltyMonths, Math.max(totalInterest, 0));
2019
+ result.earlyWithdrawalPenaltyMonths = earlyWithdrawalPenaltyMonths;
2020
+ result.earlyWithdrawalForfeitedInterest = round22(forfeited);
2021
+ result.earlyWithdrawalPayout = round22(maturityValue - forfeited);
2022
+ }
2023
+ return result;
2024
+ }
1931
2025
 
1932
- 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, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCashBreakdown, calculateChange, calculateCompoundInterest, calculateDiscount, calculateEmi, calculateLoan, calculateMarginMarkup, 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 };
2026
+ 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, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCashBreakdown, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateDiscount, calculateEmi, calculateLoan, calculateMarginMarkup, 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 };