@utilix-tech/sdk 0.97.0 → 0.99.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.99.0
6
+ - Add `calculateDividendYield` (dividend-yield-calculator): compute a stock's dividend yield from annual dividend per share and share price, plus payout ratio and retention ratio when trailing EPS is given.
7
+ - Add `checkKnownHostsFingerprints` (ssh-known-hosts-fingerprint-checker): compute SHA256/MD5 fingerprints for ssh_known_hosts entries the way `ssh-keygen -lf` does, decoding key type and RSA bit length to flag weak or deprecated keys.
8
+
9
+ ## 0.98.0
10
+ - Add `convertTimecode` (timecode-converter): convert between a frame count, SMPTE timecode (HH:MM:SS:FF), and seconds at a given frame rate, handling NTSC drop-frame timecode's skipped frame numbers correctly.
11
+ - Add `validateGeoJson` (geojson-validator): validate a GeoJSON document against RFC 7946's structural rules, geometry type/coordinates shape, polygon ring closure and winding, and a FeatureCollection's required fields.
12
+
5
13
  ## 0.97.0
6
14
  - Add `calculatePositionSize` (position-size-risk-calculator): compute a trade's position size from account size, risk percent per trade, entry price, and stop-loss price, plus the risk/reward ratio against an optional target price.
7
15
  - Add `detectWeaselWords` (weasel-word-detector): scan text for weasel words, vague hedging phrases that imply evidence or consensus without naming a checkable source, like "studies show" or "many experts believe".
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **744 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **750 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)
@@ -67,7 +67,7 @@ const { formatJson } = require("@utilix-tech/sdk/json");
67
67
  ```ts
68
68
  import { formatJson, minifyJson, diffJson, jsonToTypescript, jsonSchemaToTypescript, validateJson,
69
69
  evaluateJsonPath, resolveJsonPointer, applyJsonMergePatch, diffJsonPatch, applyJsonPatch,
70
- jsonToCsv, yamlToJson, jsonToYaml, getJsonStats } from "@utilix-tech/sdk/json";
70
+ jsonToCsv, yamlToJson, jsonToYaml, getJsonStats, validateGeoJson } from "@utilix-tech/sdk/json";
71
71
 
72
72
  // Format with 2-space indent
73
73
  formatJson('{"name":"alice","age":30}', { indent: 2 });
@@ -103,6 +103,10 @@ jsonToCsv('[{"a":1,"b":2}]');
103
103
 
104
104
  // Validate against JSON Schema
105
105
  validateJson(data, schema);
106
+
107
+ // Validate a GeoJSON document's structure against RFC 7946
108
+ validateGeoJson('{"type":"Point","coordinates":[30,10]}');
109
+ // { valid: true, type: "Point", errors: [], warnings: [] }
106
110
  ```
107
111
 
108
112
  ---
@@ -463,6 +467,10 @@ calculateVideoBitrate({ mode: "size-from-bitrate", durationSeconds: 600, videoBi
463
467
  calculateRuleOf72(8, "rate-to-years");
464
468
  // { ruleOf72Estimate: 9, exactValue: 9.0065, errorPercent: -0.0718, ... }
465
469
 
470
+ // Frame count <-> SMPTE timecode <-> seconds, with correct NTSC drop-frame handling
471
+ convertTimecode({ mode: "frames-to-timecode", frameRate: 29.97, dropFrame: true, frameCount: 17982 });
472
+ // { timecode: "00:10:00;00", frameCount: 17982, seconds: 600 }
473
+
466
474
  // EMI: equated monthly installment with a year-by-year principal/interest breakdown
467
475
  calculateEmi(500000, 8.5, 5, "years");
468
476
  // { emi: 10258.27, totalInterest: 115495.89, tenureMonths: 60, yearlyBreakdown: [...] }
@@ -802,6 +810,16 @@ calculatePositionSize({ accountSize: 10000, riskPercent: 1, entryPrice: 50, stop
802
810
  import { detectWeaselWords } from "@utilix-tech/sdk/text";
803
811
  detectWeaselWords("Studies show that many experts believe this works.");
804
812
  // { totalWords: 8, totalMatches: 2, density: 25, matches: [{ phrase: "many experts believe", count: 1, indices: [...] }, { phrase: "studies show", count: 1, indices: [...] }] }
813
+
814
+ // Dividend yield from annual dividend/share price, plus payout + retention ratio if EPS is given
815
+ import { calculateDividendYield } from "@utilix-tech/sdk/units";
816
+ calculateDividendYield({ annualDividendPerShare: 2, sharePrice: 100, epsPerShare: 5 });
817
+ // { dividendYieldPercent: 2, payoutRatioPercent: 40, retentionRatioPercent: 60, ... }
818
+
819
+ // SSH known_hosts SHA256/MD5 fingerprints (matches `ssh-keygen -lf`), flags weak RSA/ssh-dss keys
820
+ import { checkKnownHostsFingerprints } from "@utilix-tech/sdk/hashing";
821
+ checkKnownHostsFingerprints("example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPgoxEP6yy3WPraZXnSJFc9Ly5tby6R7TVyMeAOuYORR");
822
+ // { entries: [{ host: "example.com", keyType: "ssh-ed25519", fingerprintSha256: "SHA256:...", fingerprintMd5: "MD5:...", bits: 256, weak: false }] }
805
823
  ```
806
824
 
807
825
  ---
@@ -11,6 +11,7 @@ __export(hashing_exports, {
11
11
  HASH_BITS: () => HASH_BITS,
12
12
  ROUND_OPTIONS: () => ROUND_OPTIONS,
13
13
  SRI_ALGORITHMS: () => SRI_ALGORITHMS,
14
+ checkKnownHostsFingerprints: () => checkKnownHostsFingerprints,
14
15
  createEntry: () => createEntry,
15
16
  generateBcryptHash: () => generateBcryptHash,
16
17
  generateEtag: () => generateEtag,
@@ -292,5 +293,93 @@ function generateEtag(content, algorithm = "SHA-1", weak = false) {
292
293
  const etag = weak ? `W/${quoted}` : quoted;
293
294
  return { algorithm, weak, hash, etag, headerName: "ETag", snippet: `ETag: ${etag}` };
294
295
  }
296
+ function _readSshString(buf, offset) {
297
+ if (offset + 4 > buf.length) return null;
298
+ const len = buf.readUInt32BE(offset);
299
+ const start = offset + 4;
300
+ const end = start + len;
301
+ if (end > buf.length) return null;
302
+ return { value: buf.subarray(start, end), next: end };
303
+ }
304
+ function _mpintBitLength(buf) {
305
+ let i = 0;
306
+ while (i < buf.length - 1 && buf[i] === 0) i++;
307
+ const trimmed = buf.subarray(i);
308
+ if (trimmed.length === 0) return 0;
309
+ let bits = (trimmed.length - 1) * 8;
310
+ let firstByte = trimmed[0];
311
+ while (firstByte > 0) {
312
+ bits++;
313
+ firstByte >>= 1;
314
+ }
315
+ return bits;
316
+ }
317
+ function _analyzeKnownHostsKey(keyType, blob) {
318
+ if (keyType === "ssh-dss") {
319
+ return { weak: true, weakReason: "DSA (ssh-dss) keys are deprecated and considered weak" };
320
+ }
321
+ if (keyType === "ssh-rsa") {
322
+ const typeField = _readSshString(blob, 0);
323
+ if (!typeField) return { weak: false };
324
+ const eField = _readSshString(blob, typeField.next);
325
+ if (!eField) return { weak: false };
326
+ const nField = _readSshString(blob, eField.next);
327
+ if (!nField) return { weak: false };
328
+ const bits = _mpintBitLength(nField.value);
329
+ return bits > 0 && bits < 2048 ? { bits, weak: true, weakReason: `RSA modulus is only ${bits} bits; 2048+ is recommended` } : { bits, weak: false };
330
+ }
331
+ if (keyType === "ssh-ed25519") {
332
+ return { bits: 256, weak: false };
333
+ }
334
+ if (keyType.startsWith("ecdsa-sha2-")) {
335
+ const curve = keyType.split("-").pop() ?? "";
336
+ const bits = curve === "nistp256" ? 256 : curve === "nistp384" ? 384 : curve === "nistp521" ? 521 : void 0;
337
+ return { bits, weak: false };
338
+ }
339
+ return { weak: false };
340
+ }
341
+ function checkKnownHostsFingerprints(input) {
342
+ if (typeof input !== "string" || input.trim().length === 0) {
343
+ return { error: "input is required" };
344
+ }
345
+ const lines = input.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
346
+ if (lines.length === 0) {
347
+ return { error: "No known_hosts entries found" };
348
+ }
349
+ const entries = [];
350
+ for (const line of lines) {
351
+ const parts = line.split(/\s+/);
352
+ let idx = 0;
353
+ if (parts[0] === "@cert-authority" || parts[0] === "@revoked") idx = 1;
354
+ if (parts.length < idx + 3) {
355
+ return { error: `Malformed line: "${line}"` };
356
+ }
357
+ const host = parts[idx];
358
+ const keyType = parts[idx + 1];
359
+ const base64Key = parts[idx + 2];
360
+ let blob;
361
+ try {
362
+ blob = Buffer.from(base64Key, "base64");
363
+ } catch {
364
+ return { error: `Invalid base64 key for host "${host}"` };
365
+ }
366
+ if (blob.length === 0) {
367
+ return { error: `Invalid base64 key for host "${host}"` };
368
+ }
369
+ const sha256 = createHash("sha256").update(blob).digest("base64").replace(/=+$/, "");
370
+ const md52 = createHash("md5").update(blob).digest("hex").replace(/(.{2})(?=.)/g, "$1:");
371
+ const analysis = _analyzeKnownHostsKey(keyType, blob);
372
+ entries.push({
373
+ host,
374
+ keyType,
375
+ fingerprintSha256: `SHA256:${sha256}`,
376
+ fingerprintMd5: `MD5:${md52}`,
377
+ bits: analysis.bits,
378
+ weak: analysis.weak,
379
+ weakReason: analysis.weakReason
380
+ });
381
+ }
382
+ return { entries };
383
+ }
295
384
 
296
- export { ETAG_ALGORITHMS, HASH_ALGORITHMS, HASH_BITS, ROUND_OPTIONS, SRI_ALGORITHMS, createEntry, generateBcryptHash, generateEtag, generateHtpasswdFile, generateMd5Hash, generatePlain, generateSha1Hash, generateSri, generateSriAll, hashAll, hashOne, hashPassword, hashWithTiming, hashing_exports, parseHtpasswdFile, validateRounds, validateUsername, verifyPassword };
385
+ export { ETAG_ALGORITHMS, HASH_ALGORITHMS, HASH_BITS, ROUND_OPTIONS, SRI_ALGORITHMS, checkKnownHostsFingerprints, createEntry, generateBcryptHash, generateEtag, generateHtpasswdFile, generateMd5Hash, generatePlain, generateSha1Hash, generateSri, generateSriAll, hashAll, hashOne, hashPassword, hashWithTiming, hashing_exports, parseHtpasswdFile, validateRounds, validateUsername, verifyPassword };
@@ -34,6 +34,7 @@ __export(units_exports, {
34
34
  calculateDebtToIncomeRatio: () => calculateDebtToIncomeRatio,
35
35
  calculateDepreciationSchedule: () => calculateDepreciationSchedule,
36
36
  calculateDiscount: () => calculateDiscount,
37
+ calculateDividendYield: () => calculateDividendYield,
37
38
  calculateEmi: () => calculateEmi,
38
39
  calculateExtraPaymentPayoff: () => calculateExtraPaymentPayoff,
39
40
  calculateFreelancerRate: () => calculateFreelancerRate,
@@ -66,6 +67,7 @@ __export(units_exports, {
66
67
  convertAprApy: () => convertAprApy,
67
68
  convertBytes: () => convertBytes,
68
69
  convertIp: () => convertIp,
70
+ convertTimecode: () => convertTimecode,
69
71
  coverBox: () => coverBox,
70
72
  decimalToIp: () => decimalToIp,
71
73
  decimalToIpCidr: () => decimalToIpCidr,
@@ -3304,5 +3306,143 @@ function calculatePositionSize(input) {
3304
3306
  targetOnCorrectSide
3305
3307
  };
3306
3308
  }
3309
+ var TIMECODE_NOMINAL_FPS = {
3310
+ "23.976": 24,
3311
+ "24": 24,
3312
+ "25": 25,
3313
+ "29.97": 30,
3314
+ "30": 30,
3315
+ "50": 50,
3316
+ "59.94": 60,
3317
+ "60": 60
3318
+ };
3319
+ function _timecodeNominalFps(frameRate) {
3320
+ return TIMECODE_NOMINAL_FPS[String(frameRate)] ?? null;
3321
+ }
3322
+ function _timecodeIsDropFrameEligible(frameRate) {
3323
+ return frameRate === 29.97 || frameRate === 59.94;
3324
+ }
3325
+ function _framesToPlainTimecode(frameNumber, nominalFps) {
3326
+ const framesPerDay = nominalFps * 86400;
3327
+ const fn = (frameNumber % framesPerDay + framesPerDay) % framesPerDay;
3328
+ const frames = fn % nominalFps;
3329
+ const totalSeconds = Math.floor(fn / nominalFps);
3330
+ return {
3331
+ hours: Math.floor(totalSeconds / 3600) % 24,
3332
+ minutes: Math.floor(totalSeconds / 60) % 60,
3333
+ seconds: totalSeconds % 60,
3334
+ frames
3335
+ };
3336
+ }
3337
+ function _plainTimecodeToFrames(hh, mm, ss, ff, nominalFps) {
3338
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff;
3339
+ }
3340
+ function _framesToDropFrameTimecode(frameNumber, nominalFps, frameRate) {
3341
+ const dropFrames = nominalFps === 30 ? 2 : 4;
3342
+ const framesPer24Hours = Math.round(frameRate * 86400);
3343
+ const framesPer10Minutes = Math.round(frameRate * 600);
3344
+ const framesPerMinute = nominalFps * 60 - dropFrames;
3345
+ const fn = (frameNumber % framesPer24Hours + framesPer24Hours) % framesPer24Hours;
3346
+ const d = Math.floor(fn / framesPer10Minutes);
3347
+ const m = fn % framesPer10Minutes;
3348
+ let frameNumberOut = fn + 9 * dropFrames * d;
3349
+ if (m > dropFrames) {
3350
+ frameNumberOut += dropFrames * Math.floor((m - dropFrames) / framesPerMinute);
3351
+ }
3352
+ const frames = frameNumberOut % nominalFps;
3353
+ const totalSeconds = Math.floor(frameNumberOut / nominalFps);
3354
+ return {
3355
+ hours: Math.floor(totalSeconds / 3600) % 24,
3356
+ minutes: Math.floor(totalSeconds / 60) % 60,
3357
+ seconds: totalSeconds % 60,
3358
+ frames
3359
+ };
3360
+ }
3361
+ function _dropFrameTimecodeToFrames(hh, mm, ss, ff, nominalFps) {
3362
+ const dropFrames = nominalFps === 30 ? 2 : 4;
3363
+ const totalMinutes = 60 * hh + mm;
3364
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff - dropFrames * (totalMinutes - Math.floor(totalMinutes / 10));
3365
+ }
3366
+ function _formatTimecode(parts, dropFrame) {
3367
+ const p2 = (n) => String(n).padStart(2, "0");
3368
+ const sep = dropFrame ? ";" : ":";
3369
+ return `${p2(parts.hours)}:${p2(parts.minutes)}:${p2(parts.seconds)}${sep}${p2(parts.frames)}`;
3370
+ }
3371
+ function convertTimecode(input) {
3372
+ const { mode, frameRate } = input;
3373
+ const dropFrame = input.dropFrame ?? false;
3374
+ const nominalFps = _timecodeNominalFps(frameRate);
3375
+ if (nominalFps === null) {
3376
+ return { error: "frameRate must be one of 23.976, 24, 25, 29.97, 30, 50, 59.94, 60" };
3377
+ }
3378
+ if (dropFrame && !_timecodeIsDropFrameEligible(frameRate)) {
3379
+ return { error: "dropFrame is only valid for frameRate 29.97 or 59.94" };
3380
+ }
3381
+ if (mode === "frames-to-timecode") {
3382
+ const { frameCount } = input;
3383
+ if (typeof frameCount !== "number" || !Number.isFinite(frameCount) || frameCount < 0 || Math.floor(frameCount) !== frameCount) {
3384
+ return { error: 'frameCount must be a non-negative integer for mode "frames-to-timecode"' };
3385
+ }
3386
+ const parts = dropFrame ? _framesToDropFrameTimecode(frameCount, nominalFps, frameRate) : _framesToPlainTimecode(frameCount, nominalFps);
3387
+ return {
3388
+ mode,
3389
+ frameRate,
3390
+ dropFrame,
3391
+ frameCount,
3392
+ timecode: _formatTimecode(parts, dropFrame),
3393
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
3394
+ };
3395
+ }
3396
+ if (mode === "timecode-to-frames") {
3397
+ const { timecode } = input;
3398
+ if (typeof timecode !== "string") return { error: 'timecode must be a string for mode "timecode-to-frames"' };
3399
+ const match = /^(\d{2}):(\d{2}):(\d{2})[:;](\d{2})$/.exec(timecode.trim());
3400
+ if (!match) return { error: "timecode must be in the form HH:MM:SS:FF (or HH:MM:SS;FF)" };
3401
+ const [, hhS, mmS, ssS, ffS] = match;
3402
+ const hh = Number(hhS), mm = Number(mmS), ss = Number(ssS), ff = Number(ffS);
3403
+ if (hh > 23) return { error: "hours must be 00-23" };
3404
+ if (mm > 59) return { error: "minutes must be 00-59" };
3405
+ if (ss > 59) return { error: "seconds must be 00-59" };
3406
+ if (ff >= nominalFps) return { error: `frames must be 00-${nominalFps - 1} at this frame rate` };
3407
+ if (dropFrame && ff <= 1 && ss === 0 && mm % 10 !== 0) {
3408
+ return { error: `frame ${ffS} at ${hhS}:${mmS}:${ssS} does not exist in drop-frame timecode: frames 00 and 01 are dropped at the start of every minute except every 10th minute` };
3409
+ }
3410
+ const frameCount = dropFrame ? _dropFrameTimecodeToFrames(hh, mm, ss, ff, nominalFps) : _plainTimecodeToFrames(hh, mm, ss, ff, nominalFps);
3411
+ return {
3412
+ mode,
3413
+ frameRate,
3414
+ dropFrame,
3415
+ frameCount,
3416
+ timecode: _formatTimecode({ hours: hh, minutes: mm, seconds: ss, frames: ff }, dropFrame),
3417
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
3418
+ };
3419
+ }
3420
+ return { error: 'mode must be "frames-to-timecode" or "timecode-to-frames"' };
3421
+ }
3422
+ function calculateDividendYield(input) {
3423
+ const { annualDividendPerShare, sharePrice, epsPerShare } = input;
3424
+ if (!Number.isFinite(annualDividendPerShare) || annualDividendPerShare < 0) {
3425
+ return { error: "annualDividendPerShare must be a finite number greater than or equal to zero" };
3426
+ }
3427
+ if (!Number.isFinite(sharePrice) || sharePrice <= 0) {
3428
+ return { error: "sharePrice must be a finite number greater than zero" };
3429
+ }
3430
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
3431
+ const result = {
3432
+ annualDividendPerShare: round4(annualDividendPerShare),
3433
+ sharePrice: round4(sharePrice),
3434
+ dividendYieldPercent: round4(annualDividendPerShare / sharePrice * 100)
3435
+ };
3436
+ if (epsPerShare !== void 0) {
3437
+ if (!Number.isFinite(epsPerShare) || epsPerShare <= 0) {
3438
+ return { error: "epsPerShare must be a finite number greater than zero when provided" };
3439
+ }
3440
+ const payoutRatioPercent = annualDividendPerShare / epsPerShare * 100;
3441
+ result.epsPerShare = round4(epsPerShare);
3442
+ result.payoutRatioPercent = round4(payoutRatioPercent);
3443
+ result.retentionRatioPercent = round4(100 - payoutRatioPercent);
3444
+ }
3445
+ return result;
3446
+ }
3307
3447
 
3308
- 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, calculateAnnuityPayout, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditCardMinimumPayment, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateLoanPrepaymentPenalty, calculateLoanRefinance, calculateMarginMarkup, calculateMortgagePoints, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculatePositionSize, calculateRealRateOfReturn, calculateRequiredBandwidth, calculateRoi, calculateRuleOf72, calculateSalesCommission, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTaxEquivalentYield, calculateTimeValue, calculateTipSplit, calculateTransferTime, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, detectNegativeAmortization, estimateAudioFileSize, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, intersectSemverRanges, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, planDebtPayoff, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, validateIsin, validateRoutingNumber, validateVin, viewportToPx };
3448
+ 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, calculateAnnuityPayout, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditCardMinimumPayment, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateDividendYield, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateLoanPrepaymentPenalty, calculateLoanRefinance, calculateMarginMarkup, calculateMortgagePoints, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculatePositionSize, calculateRealRateOfReturn, calculateRequiredBandwidth, calculateRoi, calculateRuleOf72, calculateSalesCommission, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTaxEquivalentYield, calculateTimeValue, calculateTipSplit, calculateTransferTime, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, convertTimecode, coverBox, decimalToIp, decimalToIpCidr, describePermissions, detectNegativeAmortization, estimateAudioFileSize, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, intersectSemverRanges, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, planDebtPayoff, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, validateIsin, validateRoutingNumber, validateVin, viewportToPx };
@@ -33,6 +33,7 @@ __export(json_exports, {
33
33
  resolveJsonPointer: () => resolveJsonPointer,
34
34
  toPascalCase: () => toPascalCase,
35
35
  toSnakeCase: () => toSnakeCase,
36
+ validateGeoJson: () => validateGeoJson,
36
37
  validateJson: () => validateJson,
37
38
  validateJsonPath: () => validateJsonPath,
38
39
  validateJsonString: () => validateJsonString,
@@ -1651,5 +1652,170 @@ function generateJsonPatch(aJson, bJson) {
1651
1652
  }
1652
1653
  return { patch: diffJsonPatch(a, b) };
1653
1654
  }
1655
+ var GEOJSON_GEOMETRY_TYPES = /* @__PURE__ */ new Set([
1656
+ "Point",
1657
+ "MultiPoint",
1658
+ "LineString",
1659
+ "MultiLineString",
1660
+ "Polygon",
1661
+ "MultiPolygon",
1662
+ "GeometryCollection"
1663
+ ]);
1664
+ function _isGeoJsonNumber(v) {
1665
+ return typeof v === "number" && Number.isFinite(v);
1666
+ }
1667
+ function _isGeoJsonPosition(v) {
1668
+ return Array.isArray(v) && (v.length === 2 || v.length === 3) && v.every(_isGeoJsonNumber);
1669
+ }
1670
+ function _geoJsonPositionsEqual(a, b) {
1671
+ return a.length === b.length && a.every((v, i) => v === b[i]);
1672
+ }
1673
+ function _geoJsonRingSignedArea(ring) {
1674
+ let sum = 0;
1675
+ for (let i = 0; i < ring.length - 1; i++) {
1676
+ const [x1, y1] = ring[i];
1677
+ const [x2, y2] = ring[i + 1];
1678
+ sum += x1 * y2 - x2 * y1;
1679
+ }
1680
+ return sum / 2;
1681
+ }
1682
+ function _validateGeoJsonPolygonCoords(coords, path, errors, warnings) {
1683
+ if (!Array.isArray(coords) || coords.length === 0) {
1684
+ errors.push({ path, message: "Polygon coordinates must be a non-empty array of linear rings" });
1685
+ return;
1686
+ }
1687
+ coords.forEach((ring, i) => {
1688
+ const ringPath = `${path}[${i}]`;
1689
+ if (!Array.isArray(ring) || ring.length < 4 || !ring.every(_isGeoJsonPosition)) {
1690
+ errors.push({ path: ringPath, message: "each linear ring must have at least 4 positions" });
1691
+ return;
1692
+ }
1693
+ const first = ring[0];
1694
+ const last = ring[ring.length - 1];
1695
+ if (!_geoJsonPositionsEqual(first, last)) {
1696
+ errors.push({ path: ringPath, message: "the first and last positions of a linear ring must be identical (the ring must be closed)" });
1697
+ return;
1698
+ }
1699
+ const area = _geoJsonRingSignedArea(ring);
1700
+ const isExterior = i === 0;
1701
+ if (isExterior && area < 0) {
1702
+ warnings.push({ path: ringPath, message: "exterior ring winds clockwise; RFC 7946 recommends counterclockwise for exterior rings (right-hand rule)" });
1703
+ } else if (!isExterior && area > 0) {
1704
+ warnings.push({ path: ringPath, message: "interior ring (hole) winds counterclockwise; RFC 7946 recommends clockwise for interior rings (right-hand rule)" });
1705
+ }
1706
+ });
1707
+ }
1708
+ function _validateGeoJsonGeometry(geom, path, errors, warnings) {
1709
+ if (geom === null) return;
1710
+ if (typeof geom !== "object" || Array.isArray(geom)) {
1711
+ errors.push({ path, message: "geometry must be an object or null" });
1712
+ return;
1713
+ }
1714
+ const type = geom.type;
1715
+ if (typeof type !== "string" || !GEOJSON_GEOMETRY_TYPES.has(type)) {
1716
+ errors.push({ path: `${path}.type`, message: `geometry type must be one of ${[...GEOJSON_GEOMETRY_TYPES].join(", ")}` });
1717
+ return;
1718
+ }
1719
+ if (type === "GeometryCollection") {
1720
+ if (!Array.isArray(geom.geometries)) {
1721
+ errors.push({ path: `${path}.geometries`, message: 'GeometryCollection must have a "geometries" array' });
1722
+ return;
1723
+ }
1724
+ geom.geometries.forEach((g, i) => _validateGeoJsonGeometry(g, `${path}.geometries[${i}]`, errors, warnings));
1725
+ return;
1726
+ }
1727
+ const coords = geom.coordinates;
1728
+ const coordPath = `${path}.coordinates`;
1729
+ switch (type) {
1730
+ case "Point":
1731
+ if (!_isGeoJsonPosition(coords)) errors.push({ path: coordPath, message: "Point coordinates must be an array of 2 or 3 numbers" });
1732
+ break;
1733
+ case "MultiPoint":
1734
+ if (!Array.isArray(coords) || !coords.every(_isGeoJsonPosition)) {
1735
+ errors.push({ path: coordPath, message: "MultiPoint coordinates must be an array of positions" });
1736
+ }
1737
+ break;
1738
+ case "LineString":
1739
+ if (!Array.isArray(coords) || coords.length < 2 || !coords.every(_isGeoJsonPosition)) {
1740
+ errors.push({ path: coordPath, message: "LineString coordinates must be an array of at least 2 positions" });
1741
+ }
1742
+ break;
1743
+ case "MultiLineString":
1744
+ if (!Array.isArray(coords) || !coords.every((line) => Array.isArray(line) && line.length >= 2 && line.every(_isGeoJsonPosition))) {
1745
+ errors.push({ path: coordPath, message: "MultiLineString coordinates must be an array of LineString coordinate arrays" });
1746
+ }
1747
+ break;
1748
+ case "Polygon":
1749
+ _validateGeoJsonPolygonCoords(coords, coordPath, errors, warnings);
1750
+ break;
1751
+ case "MultiPolygon":
1752
+ if (!Array.isArray(coords)) {
1753
+ errors.push({ path: coordPath, message: "MultiPolygon coordinates must be an array of Polygon coordinate arrays" });
1754
+ } else {
1755
+ coords.forEach((poly, i) => _validateGeoJsonPolygonCoords(poly, `${coordPath}[${i}]`, errors, warnings));
1756
+ }
1757
+ break;
1758
+ }
1759
+ }
1760
+ function _validateGeoJsonBbox(bbox, path, errors) {
1761
+ if (bbox === void 0) return;
1762
+ if (!Array.isArray(bbox) || bbox.length % 2 !== 0 || bbox.length < 4 || !bbox.every(_isGeoJsonNumber)) {
1763
+ errors.push({ path: `${path}.bbox`, message: "bbox must be an array of 2*n numbers (min/max per axis)" });
1764
+ }
1765
+ }
1766
+ function _validateGeoJsonFeature(feature, path, errors, warnings) {
1767
+ if (typeof feature !== "object" || feature === null || Array.isArray(feature)) {
1768
+ errors.push({ path, message: "Feature must be an object" });
1769
+ return;
1770
+ }
1771
+ if (feature.type !== "Feature") {
1772
+ errors.push({ path: `${path}.type`, message: '"type" must be "Feature"' });
1773
+ }
1774
+ if (!("properties" in feature) || typeof feature.properties !== "object") {
1775
+ errors.push({ path: `${path}.properties`, message: '"properties" member is required and must be an object or null' });
1776
+ }
1777
+ if (!("geometry" in feature)) {
1778
+ errors.push({ path: `${path}.geometry`, message: '"geometry" member is required (an object or null)' });
1779
+ } else {
1780
+ _validateGeoJsonGeometry(feature.geometry, `${path}.geometry`, errors, warnings);
1781
+ }
1782
+ _validateGeoJsonBbox(feature.bbox, path, errors);
1783
+ }
1784
+ function validateGeoJson(input) {
1785
+ let doc;
1786
+ if (typeof input === "string") {
1787
+ try {
1788
+ doc = JSON.parse(input);
1789
+ } catch (e) {
1790
+ return { valid: false, type: null, errors: [{ path: "$", message: `invalid JSON: ${e.message}` }], warnings: [] };
1791
+ }
1792
+ } else {
1793
+ doc = input;
1794
+ }
1795
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
1796
+ return { valid: false, type: null, errors: [{ path: "$", message: "document must be a JSON object" }], warnings: [] };
1797
+ }
1798
+ const type = doc.type;
1799
+ if (typeof type !== "string") {
1800
+ return { valid: false, type: null, errors: [{ path: "$.type", message: '"type" member is required' }], warnings: [] };
1801
+ }
1802
+ const errors = [];
1803
+ const warnings = [];
1804
+ _validateGeoJsonBbox(doc.bbox, "$", errors);
1805
+ if (type === "FeatureCollection") {
1806
+ if (!Array.isArray(doc.features)) {
1807
+ errors.push({ path: "$.features", message: 'FeatureCollection must have a "features" array' });
1808
+ } else {
1809
+ doc.features.forEach((feature, i) => _validateGeoJsonFeature(feature, `$.features[${i}]`, errors, warnings));
1810
+ }
1811
+ } else if (type === "Feature") {
1812
+ _validateGeoJsonFeature(doc, "$", errors, warnings);
1813
+ } else if (GEOJSON_GEOMETRY_TYPES.has(type)) {
1814
+ _validateGeoJsonGeometry(doc, "$", errors, warnings);
1815
+ } else {
1816
+ errors.push({ path: "$.type", message: '"type" must be one of Feature, FeatureCollection, or a geometry type' });
1817
+ }
1818
+ return { valid: errors.length === 0, type, errors, warnings };
1819
+ }
1654
1820
 
1655
- export { applyJsonMergePatch, applyJsonPatch, csvToJson, detectFormat, diffJson, diffJsonPatch, evaluateJsonPath, formatDiffLine, formatJson, generateExample, generateJsonPatch, getJsonStats, inferGoType, inferPythonType, inferType, jsonSchemaToTypescript, jsonToCsv, jsonToGo, jsonToPython, jsonToTypescript, jsonToYaml, jsonToZod, json_exports, mergeJsonPatch, minifyJson, parseJson, parseStructs, resolveJsonPointer, toPascalCase, toSnakeCase, validateJson, validateJsonPath, validateJsonString, yamlToJson };
1821
+ export { applyJsonMergePatch, applyJsonPatch, csvToJson, detectFormat, diffJson, diffJsonPatch, evaluateJsonPath, formatDiffLine, formatJson, generateExample, generateJsonPatch, getJsonStats, inferGoType, inferPythonType, inferType, jsonSchemaToTypescript, jsonToCsv, jsonToGo, jsonToPython, jsonToTypescript, jsonToYaml, jsonToZod, json_exports, mergeJsonPatch, minifyJson, parseJson, parseStructs, resolveJsonPointer, toPascalCase, toSnakeCase, validateGeoJson, validateJson, validateJsonPath, validateJsonString, yamlToJson };
@@ -9,6 +9,7 @@
9
9
  * generateHtpasswdFile, validateUsername
10
10
  * - sri-hash-generator: generateSri, generateSriAll (SHA-256/384/512 Subresource Integrity hashes)
11
11
  * - etag-generator : generateEtag (MD5/SHA-1/SHA-256 content-hash ETag header values)
12
+ * - ssh-known-hosts-fingerprint-checker: checkKnownHostsFingerprints
12
13
  */
13
14
  type HashAlgorithm = 'MD5' | 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
14
15
  declare const HASH_ALGORITHMS: HashAlgorithm[];
@@ -88,6 +89,32 @@ interface EtagResult {
88
89
  declare function generateEtag(content: string, algorithm?: EtagAlgorithm, weak?: boolean): EtagResult | {
89
90
  error: string;
90
91
  };
92
+ interface KnownHostsEntry {
93
+ host: string;
94
+ keyType: string;
95
+ fingerprintSha256: string;
96
+ fingerprintMd5: string;
97
+ bits?: number;
98
+ weak: boolean;
99
+ weakReason?: string;
100
+ }
101
+ interface KnownHostsCheckResult {
102
+ entries: KnownHostsEntry[];
103
+ }
104
+ /**
105
+ * Parse an ssh_known_hosts file (or a single `host keytype base64key
106
+ * [comment]` line) and compute each entry's SHA256 and MD5 fingerprint the
107
+ * same way `ssh-keygen -lf` does: SHA256 is the base64 (no padding) of the
108
+ * SHA-256 digest of the decoded key blob, MD5 is the colon-separated hex of
109
+ * the MD5 digest. Also decodes the key type and, for RSA, the modulus bit
110
+ * length, flagging a weak RSA key (<2048 bits) or a deprecated ssh-dss
111
+ * (DSA) key. `@cert-authority`/`@revoked` markers are skipped. Operates
112
+ * only on public key material already present in the file; never touches a
113
+ * private key.
114
+ */
115
+ declare function checkKnownHostsFingerprints(input: string): KnownHostsCheckResult | {
116
+ error: string;
117
+ };
91
118
 
92
119
  type hashing_BcryptResult = BcryptResult;
93
120
  declare const hashing_ETAG_ALGORITHMS: typeof ETAG_ALGORITHMS;
@@ -99,10 +126,13 @@ type hashing_HashAlgorithm = HashAlgorithm;
99
126
  type hashing_HashResult = HashResult;
100
127
  type hashing_HtpasswdAlgorithm = HtpasswdAlgorithm;
101
128
  type hashing_HtpasswdEntry = HtpasswdEntry;
129
+ type hashing_KnownHostsCheckResult = KnownHostsCheckResult;
130
+ type hashing_KnownHostsEntry = KnownHostsEntry;
102
131
  declare const hashing_ROUND_OPTIONS: typeof ROUND_OPTIONS;
103
132
  declare const hashing_SRI_ALGORITHMS: typeof SRI_ALGORITHMS;
104
133
  type hashing_SriAlgorithm = SriAlgorithm;
105
134
  type hashing_SriResult = SriResult;
135
+ declare const hashing_checkKnownHostsFingerprints: typeof checkKnownHostsFingerprints;
106
136
  declare const hashing_createEntry: typeof createEntry;
107
137
  declare const hashing_generateBcryptHash: typeof generateBcryptHash;
108
138
  declare const hashing_generateEtag: typeof generateEtag;
@@ -121,7 +151,7 @@ declare const hashing_validateRounds: typeof validateRounds;
121
151
  declare const hashing_validateUsername: typeof validateUsername;
122
152
  declare const hashing_verifyPassword: typeof verifyPassword;
123
153
  declare namespace hashing {
124
- export { type hashing_BcryptResult as BcryptResult, hashing_ETAG_ALGORITHMS as ETAG_ALGORITHMS, type hashing_EtagAlgorithm as EtagAlgorithm, type hashing_EtagResult as EtagResult, hashing_HASH_ALGORITHMS as HASH_ALGORITHMS, hashing_HASH_BITS as HASH_BITS, type hashing_HashAlgorithm as HashAlgorithm, type hashing_HashResult as HashResult, type hashing_HtpasswdAlgorithm as HtpasswdAlgorithm, type hashing_HtpasswdEntry as HtpasswdEntry, hashing_ROUND_OPTIONS as ROUND_OPTIONS, hashing_SRI_ALGORITHMS as SRI_ALGORITHMS, type hashing_SriAlgorithm as SriAlgorithm, type hashing_SriResult as SriResult, hashing_createEntry as createEntry, hashing_generateBcryptHash as generateBcryptHash, hashing_generateEtag as generateEtag, hashing_generateHtpasswdFile as generateHtpasswdFile, hashing_generateMd5Hash as generateMd5Hash, hashing_generatePlain as generatePlain, hashing_generateSha1Hash as generateSha1Hash, hashing_generateSri as generateSri, hashing_generateSriAll as generateSriAll, hashing_hashAll as hashAll, hashing_hashOne as hashOne, hashing_hashPassword as hashPassword, hashing_hashWithTiming as hashWithTiming, hashing_parseHtpasswdFile as parseHtpasswdFile, hashing_validateRounds as validateRounds, hashing_validateUsername as validateUsername, hashing_verifyPassword as verifyPassword };
154
+ export { type hashing_BcryptResult as BcryptResult, hashing_ETAG_ALGORITHMS as ETAG_ALGORITHMS, type hashing_EtagAlgorithm as EtagAlgorithm, type hashing_EtagResult as EtagResult, hashing_HASH_ALGORITHMS as HASH_ALGORITHMS, hashing_HASH_BITS as HASH_BITS, type hashing_HashAlgorithm as HashAlgorithm, type hashing_HashResult as HashResult, type hashing_HtpasswdAlgorithm as HtpasswdAlgorithm, type hashing_HtpasswdEntry as HtpasswdEntry, type hashing_KnownHostsCheckResult as KnownHostsCheckResult, type hashing_KnownHostsEntry as KnownHostsEntry, hashing_ROUND_OPTIONS as ROUND_OPTIONS, hashing_SRI_ALGORITHMS as SRI_ALGORITHMS, type hashing_SriAlgorithm as SriAlgorithm, type hashing_SriResult as SriResult, hashing_checkKnownHostsFingerprints as checkKnownHostsFingerprints, hashing_createEntry as createEntry, hashing_generateBcryptHash as generateBcryptHash, hashing_generateEtag as generateEtag, hashing_generateHtpasswdFile as generateHtpasswdFile, hashing_generateMd5Hash as generateMd5Hash, hashing_generatePlain as generatePlain, hashing_generateSha1Hash as generateSha1Hash, hashing_generateSri as generateSri, hashing_generateSriAll as generateSriAll, hashing_hashAll as hashAll, hashing_hashOne as hashOne, hashing_hashPassword as hashPassword, hashing_hashWithTiming as hashWithTiming, hashing_parseHtpasswdFile as parseHtpasswdFile, hashing_validateRounds as validateRounds, hashing_validateUsername as validateUsername, hashing_verifyPassword as verifyPassword };
125
155
  }
126
156
 
127
- export { verifyPassword as A, type BcryptResult as B, ETAG_ALGORITHMS as E, HASH_ALGORITHMS as H, ROUND_OPTIONS as R, SRI_ALGORITHMS as S, type EtagAlgorithm as a, type EtagResult as b, HASH_BITS as c, type HashAlgorithm as d, type HashResult as e, type HtpasswdAlgorithm as f, type HtpasswdEntry as g, hashing as h, type SriAlgorithm as i, type SriResult as j, createEntry as k, generateBcryptHash as l, generateEtag as m, generateHtpasswdFile as n, generateMd5Hash as o, generatePlain as p, generateSha1Hash as q, generateSri as r, generateSriAll as s, hashAll as t, hashOne as u, hashPassword as v, hashWithTiming as w, parseHtpasswdFile as x, validateRounds as y, validateUsername as z };
157
+ export { validateRounds as A, type BcryptResult as B, validateUsername as C, verifyPassword as D, ETAG_ALGORITHMS as E, HASH_ALGORITHMS as H, type KnownHostsCheckResult as K, ROUND_OPTIONS as R, SRI_ALGORITHMS as S, type EtagAlgorithm as a, type EtagResult as b, HASH_BITS as c, type HashAlgorithm as d, type HashResult as e, type HtpasswdAlgorithm as f, type HtpasswdEntry as g, hashing as h, type KnownHostsEntry as i, type SriAlgorithm as j, type SriResult as k, checkKnownHostsFingerprints as l, createEntry as m, generateBcryptHash as n, generateEtag as o, generateHtpasswdFile as p, generateMd5Hash as q, generatePlain as r, generateSha1Hash as s, generateSri as t, generateSriAll as u, hashAll as v, hashOne as w, hashPassword as x, hashWithTiming as y, parseHtpasswdFile as z };