From 3ab5d9e5c246be2b6a7abe1b8edcb8983d0b15ca Mon Sep 17 00:00:00 2001 From: fraxken Date: Sun, 3 Sep 2023 17:17:26 +0200 Subject: [PATCH 1/4] refactor!: cleanup codebase & revampirize UT --- .gitignore | 2 + LICENSE | 2 +- src/index.d.ts => index.d.ts | 0 src/index.js => index.js | 76 ++++++++-------- package.json | 92 ++++++++++---------- temp.js | 11 --- test/extractAllAuthors.spec.js | 52 +++++++++++ test/{ => fixtures}/nsecure-result.json | 0 test/index.js | 54 ------------ test/{levenshtein.js => levenshtein.spec.js} | 34 ++++---- 10 files changed, 157 insertions(+), 166 deletions(-) rename src/index.d.ts => index.d.ts (100%) rename src/index.js => index.js (86%) delete mode 100644 temp.js create mode 100644 test/extractAllAuthors.spec.js rename test/{ => fixtures}/nsecure-result.json (100%) delete mode 100644 test/index.js rename test/{levenshtein.js => levenshtein.spec.js} (74%) diff --git a/.gitignore b/.gitignore index 6704566..2518495 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ dist # TernJS port file .tern-port + +/temp diff --git a/LICENSE b/LICENSE index 90374c9..346097d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 NodeSecure +Copyright (c) 2023 NodeSecure Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/index.d.ts b/index.d.ts similarity index 100% rename from src/index.d.ts rename to index.d.ts diff --git a/src/index.js b/index.js similarity index 86% rename from src/index.js rename to index.js index 7f9a300..a39bfe3 100644 --- a/src/index.js +++ b/index.js @@ -1,28 +1,16 @@ -// Import Internal Dependencies -import { useLevenshtein } from "./levenshtein.js"; -import { getDomainExpirationFromMemory, storeDomainExpirationInMemory } from "./helper.js"; +// Import Third-party Dependencies import { whois, resolveMxRecords } from "@nodesecure/domain-check"; -function splitAuthorNameEmail(author) { - const indexStartEmail = author.name.search(/[<]/g); - const indexEndEmail = author.name.search(/[>]/g); - - if (indexStartEmail === -1 && indexEndEmail === -1) { - return { - name: author.name, - email: "email" in author ? author.email : "" - }; - } - - return { - name: author.substring(0, indexStartEmail).trim(), - email: author.substring(indexStartEmail, indexEndEmail).trim() - }; -} +// Import Internal Dependencies +import { useLevenshtein } from "./src/levenshtein.js"; +import { getDomainExpirationFromMemory, storeDomainExpirationInMemory } from "./src/helper.js"; -export async function extractAllAuthors(library, opts = { flags: [], domainInformations: false }) { +export async function extractAllAuthors( + library, + opts = { flags: [], domainInformations: false } +) { if (!("dependencies" in library)) { - return []; + throw new Error("You must provide a list of dependencies"); } const authors = []; @@ -57,13 +45,18 @@ export async function extractAllAuthors(library, opts = { flags: [], domainInfor } } if (authors.length === 0) { - return []; + return { + authorsFlagged: [], + authors: [] + }; } - const authorsFlagged = findFlaggedAuthors(useLevenshtein(authors), opts.flags); - - if (opts.domainInformations === true) { - return addDomainInformations(authors); + const authorsFlagged = findFlaggedAuthors( + useLevenshtein(authors), + opts.flags + ); + if (opts.domainInformations) { + addDomainInformations(authors); } return { @@ -100,7 +93,6 @@ async function addDomainInformations(authors) { return authors; } - function findFlaggedAuthors(authors, flags) { const res = []; for (const author of authors) { @@ -114,6 +106,17 @@ function findFlaggedAuthors(authors, flags) { return res; } +function formatAuthors({ author, maintainers, publishers }) { + const authors = []; + + if (author?.name !== undefined) { + authors.push(splitAuthorNameEmail(author)); + } + iterateOver(maintainers, authors); + iterateOver(publishers, authors); + + return useLevenshtein(authors); +} function iterateOver(iterable, arrayAuthors) { for (const contributor of iterable) { @@ -135,14 +138,19 @@ function iterateOver(iterable, arrayAuthors) { } } -function formatAuthors({ author, maintainers, publishers }) { - const authors = []; +function splitAuthorNameEmail(author) { + const indexStartEmail = author.name.search(/[<]/g); + const indexEndEmail = author.name.search(/[>]/g); - if (author?.name !== undefined) { - authors.push(splitAuthorNameEmail(author)); + if (indexStartEmail === -1 && indexEndEmail === -1) { + return { + name: author.name, + email: "email" in author ? author.email : "" + }; } - iterateOver(maintainers, authors); - iterateOver(publishers, authors); - return useLevenshtein(authors); + return { + name: author.substring(0, indexStartEmail).trim(), + email: author.substring(indexStartEmail, indexEndEmail).trim() + }; } diff --git a/package.json b/package.json index 72fc811..8981b31 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,45 @@ -{ - "name": "@nodesecure/authors", - "version": "1.0.2", - "description": "NodeSecure (npm) authors analysis package", - "exports": "./src/index.js", - "type": "module", - "types": "./src/index.d.ts", - "scripts": { - "lint": "cross-env eslint ./src", - "test-only": "cross-env esm-tape-runner 'test/**/*.js' | tap-monkey", - "test": "npm run lint && npm run test-only", - "coverage": "c8 -r html npm test" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/NodeSecure/authors.git" - }, - "keywords": [ - "NodeSecure", - "authors", - "author", - "npm" - ], - "files": [ - "src" - ], - "author": "GENTILHOMME Thomas ", - "license": "MIT", - "bugs": { - "url": "https://github.com/NodeSecure/authors/issues" - }, - "homepage": "https://github.com/NodeSecure/authors#readme", - "devDependencies": { - "@nodesecure/eslint-config": "^1.6.0", - "@nodesecure/scanner": "^3.8.2", - "@npm/types": "^1.0.2", - "@small-tech/esm-tape-runner": "^2.0.0", - "@small-tech/tap-monkey": "^1.4.0", - "c8": "^7.12.0", - "cross-env": "^7.0.3", - "tape": "^5.6.3" - }, - "dependencies": { - "@myunisoft/httpie": "^1.10.0", - "@nodesecure/domain-check": "^1.0.1" - } -} +{ + "name": "@nodesecure/authors", + "version": "1.0.2", + "description": "NodeSecure (npm) authors analysis package", + "exports": "./index.js", + "type": "module", + "types": "./index.d.ts", + "scripts": { + "lint": "cross-env eslint ./src", + "test-only": "node --test test/", + "test": "npm run lint && npm run test-only", + "coverage": "c8 -r html npm test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NodeSecure/authors.git" + }, + "keywords": [ + "NodeSecure", + "authors", + "author", + "npm" + ], + "files": [ + "index.js", + "index.d.ts", + "src" + ], + "author": "GENTILHOMME Thomas ", + "license": "MIT", + "bugs": { + "url": "https://github.com/NodeSecure/authors/issues" + }, + "homepage": "https://github.com/NodeSecure/authors#readme", + "devDependencies": { + "@nodesecure/eslint-config": "^1.8.0", + "@nodesecure/scanner": "^4.0.0", + "@npm/types": "^1.0.2", + "c8": "^8.0.1" + }, + "dependencies": { + "@myunisoft/httpie": "^2.0.1", + "@nodesecure/domain-check": "^1.0.1" + } +} diff --git a/temp.js b/temp.js deleted file mode 100644 index 39d5ae1..0000000 --- a/temp.js +++ /dev/null @@ -1,11 +0,0 @@ -import { from } from "@nodesecure/scanner"; -import { writeFileSync } from "fs"; - -async function main() { - const result = await from("express", { - maxDepth: 10 - }); - - writeFileSync("./test/nsecure-result.json", JSON.stringify(result, null, 2)); -} -main().catch(console.error); diff --git a/test/extractAllAuthors.spec.js b/test/extractAllAuthors.spec.js new file mode 100644 index 0000000..950e413 --- /dev/null +++ b/test/extractAllAuthors.spec.js @@ -0,0 +1,52 @@ +// Import Node.js Dependencies +import { test } from "node:test"; +import assert from "node:assert"; +import { readFileSync } from "node:fs"; + +// Import Internal Dependencies +import { extractAllAuthors } from "../index.js"; + +// CONSTANTS +const kFixtureNodeSecurePayload = JSON.parse( + readFileSync( + new URL("./fixtures/nsecure-result.json", import.meta.url) + ) +); + +// test("All authors from library without flags involved", async() => { +// const res = await extractAllAuthors(kFixtureNodeSecurePayload, { +// flags: [], +// domainInformations: true +// }); +// console.log(res); + +// assert.strictEqual(res.authors.length, 0, "There should be authors in the response"); +// }); + +test("test authors from library with flag", async() => { + const flaggedAuthors = [ + { name: "kesla", email: "david.bjorklund@gmail.com" } + ]; + const res = await extractAllAuthors(kFixtureNodeSecurePayload, { + flags: flaggedAuthors + }); + + assert.deepEqual(res.authorsFlagged, flaggedAuthors); + assert.deepEqual(res.authors.slice(1, 2), + [ + { + name: "kesla", + email: "david.bjorklund@gmail.com", + packages: + [ + { + homepage: "https://github.com/jshttp/etag#readme", + spec: "etag", + versions: "1.8.1", + at: "2014-05-18T11:14:58.281Z" + } + ] + } + ] + ); +}); diff --git a/test/nsecure-result.json b/test/fixtures/nsecure-result.json similarity index 100% rename from test/nsecure-result.json rename to test/fixtures/nsecure-result.json diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 86a6fe4..0000000 --- a/test/index.js +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable max-len */ -// Import Node.js Dependencies -import { readFile } from "fs/promises"; - -// Import Third-party Dependencies -import test from "tape"; - -// Import Internal Dependencies -import { extractAllAuthors } from "../src/index.js"; - -const nsecureTestFile = JSON.parse( - await readFile( - new URL("./nsecure-result.json", import.meta.url) - ) -); - -test("All authors from library without flags involved", async(tape) => { - const packageTest = nsecureTestFile; - - const res = await extractAllAuthors(packageTest, { flags: [], domainInformations: true }); - - tape.isNot(res.authors.length, 0, "There should be authors in the response"); - tape.end(); -}); - -test("test authors from library with flag", async(tape) => { - const packageTest = nsecureTestFile; - const flaggedAuthors = [ - { name: "kesla", email: "david.bjorklund@gmail.com" } - ]; - const res = await extractAllAuthors(packageTest, { - flags: flaggedAuthors - }); - - tape.deepEqual(res.authorsFlagged, flaggedAuthors); - tape.deepEqual(res.authors.slice(1, 2), - [ - { - name: "kesla", - email: "david.bjorklund@gmail.com", - packages: - [ - { - homepage: "https://github.com/jshttp/etag#readme", - spec: "etag", - versions: "1.8.1", - at: "2014-05-18T11:14:58.281Z" - } - ] - } - ] - ); - tape.end(); -}); diff --git a/test/levenshtein.js b/test/levenshtein.spec.js similarity index 74% rename from test/levenshtein.js rename to test/levenshtein.spec.js index 444de57..5823178 100644 --- a/test/levenshtein.js +++ b/test/levenshtein.spec.js @@ -1,75 +1,71 @@ -// Import Third-party Dependencies -import test from "tape"; +// Import Node.js Dependencies +import { test } from "node:test"; +import assert from "node:assert"; // Import Internal Dependencies import { isSimilar, separateWord, useLevenshtein } from "../src/levenshtein.js"; -test("check separateWord", (tape) => { +test("check separateWord", () => { const author = "Vincent Dhennin"; const author2 = "poppins,virk"; const data = separateWord(author); - tape.deepEqual(data, [ + assert.deepEqual(data, [ "Vincent", "Dhennin" ]); const data2 = separateWord(author2); - tape.deepEqual(data2, [ + assert.deepEqual(data2, [ "poppins", "virk" ]); - tape.end(); }); -test("check isSimilar about email", (tape) => { +test("check isSimilar about email", () => { const author1 = "shtylman@gmail.com"; const author2 = "doug@somethingdoug.com"; const similar = isSimilar(author1, author2); - tape.equal(similar, 16); + assert.equal(similar, 16); const author3 = "shtylman@gmail.com"; const author4 = "shtylman@gmail.com"; const similar2 = isSimilar(author3, author4); - tape.equal(similar2, 0); - tape.end(); + assert.equal(similar2, 0); }); -test("check isSimilar about names", (tape) => { +test("check isSimilar about names", () => { const author1 = "Roman Shtylman"; const author2 = "doug@somethingdoug.com"; const similar = isSimilar(author1, author2); - tape.equal(similar, 19); - tape.end(); + assert.equal(similar, 19); }); -test("useLevenshtein on authors list", (tape) => { +test("useLevenshtein on authors list", () => { const authors = [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" }, { name: "shtylman", email: "shtylman@gmail.com" } ]; const authorsFormatted = useLevenshtein(authors); - tape.deepEqual(authorsFormatted, [ + assert.deepEqual(authorsFormatted, [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" } ]); - tape.end(); }); -test("useLevenshtein on authors list containing duplicate email", (tape) => { +test("useLevenshtein on authors list containing duplicate email", () => { const authors = [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" }, { name: "shtylman", email: "shtylman@gmail.com" } ]; const authorsFormatted = useLevenshtein(authors); - tape.deepEqual(authorsFormatted, [ + assert.deepEqual(authorsFormatted, [ { name: "Roman Shtylman", email: "shtylman@gmail.com" }, { name: "dougwilson", email: "doug@somethingdoug.com" } ]); - tape.end(); }); From d4f3157a81a6745cea2500efa5674f541ca2fcb6 Mon Sep 17 00:00:00 2001 From: fraxken Date: Sun, 3 Sep 2023 17:54:47 +0200 Subject: [PATCH 2/4] chore: re-implement domain-check in author --- index.d.ts | 2 +- index.js | 83 +++++++--------------------------- package.json | 2 +- src/dns.js | 18 ++++++++ src/levenshtein.js | 10 ++-- src/utils.js | 51 +++++++++++++++++++++ src/whois.js | 41 +++++++++++++++++ test/extractAllAuthors.spec.js | 17 ++++--- 8 files changed, 142 insertions(+), 82 deletions(-) create mode 100644 src/dns.js create mode 100644 src/utils.js create mode 100644 src/whois.js diff --git a/index.d.ts b/index.d.ts index 2bbee3c..c428afc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,7 +4,7 @@ import { Scanner } from "@nodesecure/scanner"; export function extractAllAuthors(library: Scanner.Payload, opts: options): Promise export interface options { - flags: extractedAuthor[], + flaggedAuthors: extractedAuthor[], domainInformations: boolean, } diff --git a/index.js b/index.js index a39bfe3..afcee42 100644 --- a/index.js +++ b/index.js @@ -1,20 +1,19 @@ -// Import Third-party Dependencies -import { whois, resolveMxRecords } from "@nodesecure/domain-check"; - // Import Internal Dependencies +import { whois } from "./src/whois.js"; +import { resolveMxRecords } from "./src/dns.js"; import { useLevenshtein } from "./src/levenshtein.js"; import { getDomainExpirationFromMemory, storeDomainExpirationInMemory } from "./src/helper.js"; +import * as utils from "./src/utils.js"; export async function extractAllAuthors( library, - opts = { flags: [], domainInformations: false } + opts = { flaggedAuthors: [], domainInformations: false } ) { if (!("dependencies" in library)) { throw new Error("You must provide a list of dependencies"); } const authors = []; - for (let index = 0; index < Object.values(library.dependencies).length; index++) { const currPackage = { packageName: Object.keys(library.dependencies)[index], @@ -28,7 +27,7 @@ export async function extractAllAuthors( versions: currPackage.metadata.lastVersion }; - const authorsFound = formatAuthors({ author, maintainers, publishers }); + const authorsFound = utils.formatAuthors({ author, maintainers, publishers }); for (const author of authorsFound) { if (author === undefined) { @@ -51,10 +50,10 @@ export async function extractAllAuthors( }; } - const authorsFlagged = findFlaggedAuthors( + const authorsFlagged = Array.from(findFlaggedAuthors( useLevenshtein(authors), - opts.flags - ); + opts.flaggedAuthors + )); if (opts.domainInformations) { addDomainInformations(authors); } @@ -71,7 +70,11 @@ async function addDomainInformations(authors) { continue; } const domain = author.email.split("@")[1]; - const mxRecords = await resolveMxRecords(domain); + const mxRecordsResult = await resolveMxRecords(domain); + if (!mxRecordsResult.ok) { + continue; + } + const mxRecords = mxRecordsResult.safeUnwrap(); if (getDomainExpirationFromMemory(domain) !== undefined) { author.domain = { @@ -93,64 +96,12 @@ async function addDomainInformations(authors) { return authors; } -function findFlaggedAuthors(authors, flags) { - const res = []; +function* findFlaggedAuthors(authors, flaggedAuthors = []) { for (const author of authors) { - for (const flag of flags) { - if (flag.name === author.name || flag.email === author.email) { - res.push({ name: author.name, email: author.email }); - } - } - } - - return res; -} - -function formatAuthors({ author, maintainers, publishers }) { - const authors = []; - - if (author?.name !== undefined) { - authors.push(splitAuthorNameEmail(author)); - } - iterateOver(maintainers, authors); - iterateOver(publishers, authors); - - return useLevenshtein(authors); -} - -function iterateOver(iterable, arrayAuthors) { - for (const contributor of iterable) { - if (arrayAuthors.find((el) => el.name === contributor.name)) { - const index = arrayAuthors.findIndex((el) => el.name === contributor.name); - - if (arrayAuthors[index].email && arrayAuthors[index].name) { - if (contributor.at && contributor.version) { - arrayAuthors[index].at = contributor.at; - arrayAuthors[index].version = contributor.version; - } - continue; + for (const flaggedAuthor of flaggedAuthors) { + if (flaggedAuthor.name === author.name || flaggedAuthor.email === author.email) { + yield { name: author.name, email: author.email }; } - arrayAuthors[index] = contributor; } - else { - arrayAuthors.push(contributor); - } - } -} - -function splitAuthorNameEmail(author) { - const indexStartEmail = author.name.search(/[<]/g); - const indexEndEmail = author.name.search(/[>]/g); - - if (indexStartEmail === -1 && indexEndEmail === -1) { - return { - name: author.name, - email: "email" in author ? author.email : "" - }; } - - return { - name: author.substring(0, indexStartEmail).trim(), - email: author.substring(indexStartEmail, indexEndEmail).trim() - }; } diff --git a/package.json b/package.json index 8981b31..34f957c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ }, "dependencies": { "@myunisoft/httpie": "^2.0.1", - "@nodesecure/domain-check": "^1.0.1" + "@openally/result": "^1.2.0" } } diff --git a/src/dns.js b/src/dns.js new file mode 100644 index 0000000..1d7102b --- /dev/null +++ b/src/dns.js @@ -0,0 +1,18 @@ +// Import Node.js Dependencies +import dns from "node:dns/promises"; + +// Import Third-party Dependencies +import { Ok, Err } from "@openally/result"; + +export async function resolveMxRecords(domain) { + try { + const mxRecords = await dns.resolveMx(domain); + + return Ok( + mxRecords.map(({ exchange }) => exchange) + ); + } + catch (error) { + return Err(error); + } +} diff --git a/src/levenshtein.js b/src/levenshtein.js index 4cb9a61..fa29073 100644 --- a/src/levenshtein.js +++ b/src/levenshtein.js @@ -1,5 +1,5 @@ -// Constant -const KMaxLevenshtein = 2; +// CONSTANTS +const kMaxLevenshteinDistance = 2; export function separateWord(word) { const separators = [",", " ", "."]; @@ -19,7 +19,7 @@ export function separateWord(word) { export function isSimilar(firstWord, secondWord, isWordSeparated = false) { if (!firstWord || !secondWord) { - return KMaxLevenshtein; + return kMaxLevenshteinDistance; } const word1 = firstWord.toLowerCase(); const word2 = secondWord.toLowerCase(); @@ -72,8 +72,8 @@ export function useLevenshtein(authors) { const currAuthor = authors[index]; for (const author of authorsResponse) { - if (isSimilar(author.email, currAuthor.email) < KMaxLevenshtein - || isSimilar(author.name, currAuthor.name, true) < KMaxLevenshtein) { + if (isSimilar(author.email, currAuthor.email) < kMaxLevenshteinDistance + || isSimilar(author.name, currAuthor.name, true) < kMaxLevenshteinDistance) { author.email = author.email.length > currAuthor.email.length ? author.email : currAuthor.email; author.name = author.name.length > currAuthor.name.length ? author.name : currAuthor.name; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..1ba38a3 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,51 @@ +// Import Internal Dependencies +import { useLevenshtein } from "./levenshtein.js"; + +export function formatAuthors({ author, maintainers, publishers }) { + const authors = []; + + if (author?.name !== undefined) { + authors.push(splitAuthorNameEmail(author)); + } + iterateOver(maintainers, authors); + iterateOver(publishers, authors); + + return useLevenshtein(authors); +} + +export function iterateOver(iterable, arrayAuthors) { + for (const contributor of iterable) { + if (arrayAuthors.find((el) => el.name === contributor.name)) { + const index = arrayAuthors.findIndex((el) => el.name === contributor.name); + + if (arrayAuthors[index].email && arrayAuthors[index].name) { + if (contributor.at && contributor.version) { + arrayAuthors[index].at = contributor.at; + arrayAuthors[index].version = contributor.version; + } + continue; + } + arrayAuthors[index] = contributor; + } + else { + arrayAuthors.push(contributor); + } + } +} + +export function splitAuthorNameEmail(author) { + const indexStartEmail = author.name.search(/[<]/g); + const indexEndEmail = author.name.search(/[>]/g); + + if (indexStartEmail === -1 && indexEndEmail === -1) { + return { + name: author.name, + email: "email" in author ? author.email : "" + }; + } + + return { + name: author.substring(0, indexStartEmail).trim(), + email: author.substring(indexStartEmail, indexEndEmail).trim() + }; +} diff --git a/src/whois.js b/src/whois.js new file mode 100644 index 0000000..fdb243a --- /dev/null +++ b/src/whois.js @@ -0,0 +1,41 @@ +// Node.js Dependencies +import net from "node:net"; +import streamConsumers from "node:stream/consumers"; + +// CONSTANTS +const kDefaultSocketServer = "whois.iana.org"; + +function* lazyParseIanaWhoisResponse(rawResponseStr) { + /** @type {string[]} */ + const lines = rawResponseStr.split(/\r?\n/); + + for (const line of lines) { + const safeLine = line.trim(); + + if (safeLine !== "" && safeLine.charAt(0) !== "%" && safeLine.includes(":")) { + const [key, value] = safeLine.split(":"); + + yield [key.trimStart(), value.trimStart()]; + } + } +} + +export async function whois(domain, server = kDefaultSocketServer) { + const client = new net.Socket(); + client.setTimeout(1_000); + setImmediate(() => client.connect(43, server, () => client.write(`${domain}\r\n`))); + + try { + const rawResponseStr = await streamConsumers.text(client); + const response = Object.fromEntries(lazyParseIanaWhoisResponse(rawResponseStr)); + + if ("refer" in response && response.refer !== server) { + return whois(domain, response.refer); + } + + return response["Registry Expiry Date"]; + } + finally { + client.destroy(); + } +} diff --git a/test/extractAllAuthors.spec.js b/test/extractAllAuthors.spec.js index 950e413..e6535c4 100644 --- a/test/extractAllAuthors.spec.js +++ b/test/extractAllAuthors.spec.js @@ -13,22 +13,21 @@ const kFixtureNodeSecurePayload = JSON.parse( ) ); -// test("All authors from library without flags involved", async() => { -// const res = await extractAllAuthors(kFixtureNodeSecurePayload, { -// flags: [], -// domainInformations: true -// }); -// console.log(res); +test("All authors from library without flags involved", async() => { + const res = await extractAllAuthors(kFixtureNodeSecurePayload, { + flags: [], + domainInformations: true + }); -// assert.strictEqual(res.authors.length, 0, "There should be authors in the response"); -// }); + assert.ok(res.authors.length > 0, "There should be authors in the response"); +}); test("test authors from library with flag", async() => { const flaggedAuthors = [ { name: "kesla", email: "david.bjorklund@gmail.com" } ]; const res = await extractAllAuthors(kFixtureNodeSecurePayload, { - flags: flaggedAuthors + flaggedAuthors }); assert.deepEqual(res.authorsFlagged, flaggedAuthors); From 21ed554a879faca3052867e3e66d8bd2df936922 Mon Sep 17 00:00:00 2001 From: fraxken Date: Mon, 4 Sep 2023 15:45:35 +0200 Subject: [PATCH 3/4] chore: refactor whois & add getDomainsInformations --- index.js | 26 +++-------------- src/dns.js | 18 ------------ src/domains.js | 47 ++++++++++++++++++++++++++++++ src/helper.js | 9 ------ src/whois.js | 9 ++++-- test/getDomainInformations.spec.js | 30 +++++++++++++++++++ test/whois.spec.js | 23 +++++++++++++++ 7 files changed, 110 insertions(+), 52 deletions(-) delete mode 100644 src/dns.js create mode 100644 src/domains.js delete mode 100644 src/helper.js create mode 100644 test/getDomainInformations.spec.js create mode 100644 test/whois.spec.js diff --git a/index.js b/index.js index afcee42..a20e291 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ // Import Internal Dependencies -import { whois } from "./src/whois.js"; -import { resolveMxRecords } from "./src/dns.js"; +import { getDomainInformations } from "./src/domains.js"; import { useLevenshtein } from "./src/levenshtein.js"; import { getDomainExpirationFromMemory, storeDomainExpirationInMemory } from "./src/helper.js"; import * as utils from "./src/utils.js"; @@ -55,7 +54,7 @@ export async function extractAllAuthors( opts.flaggedAuthors )); if (opts.domainInformations) { - addDomainInformations(authors); + await addDomainInformations(authors); } return { @@ -70,27 +69,10 @@ async function addDomainInformations(authors) { continue; } const domain = author.email.split("@")[1]; - const mxRecordsResult = await resolveMxRecords(domain); - if (!mxRecordsResult.ok) { - continue; - } - const mxRecords = mxRecordsResult.safeUnwrap(); - - if (getDomainExpirationFromMemory(domain) !== undefined) { - author.domain = { - expirationDate: getDomainExpirationFromMemory(domain), - mxRecords - }; - continue; + if (domain) { + author.domain = await getDomainInformations(domain); } - - const expirationDate = await whois(domain); - storeDomainExpirationInMemory({ domain, expirationDate }); - author.domain = { - expirationDate, - mxRecords - }; } return authors; diff --git a/src/dns.js b/src/dns.js deleted file mode 100644 index 1d7102b..0000000 --- a/src/dns.js +++ /dev/null @@ -1,18 +0,0 @@ -// Import Node.js Dependencies -import dns from "node:dns/promises"; - -// Import Third-party Dependencies -import { Ok, Err } from "@openally/result"; - -export async function resolveMxRecords(domain) { - try { - const mxRecords = await dns.resolveMx(domain); - - return Ok( - mxRecords.map(({ exchange }) => exchange) - ); - } - catch (error) { - return Err(error); - } -} diff --git a/src/domains.js b/src/domains.js new file mode 100644 index 0000000..d04dd1d --- /dev/null +++ b/src/domains.js @@ -0,0 +1,47 @@ +// Import Node.js Dependencies +import dns from "node:dns/promises"; + +// Import Third-party Dependencies +import { Some, None } from "@openally/result"; + +// Import Internal Dependencies +import { whois } from "./whois.js"; + +// VARS +export const DOMAINS_CACHE = new Map(); + +export async function getDomainInformations(domain) { + if (DOMAINS_CACHE.has(domain)) { + return DOMAINS_CACHE.get(domain); + } + + const mxRecords = await safeResolveMx(domain); + if (mxRecords.none) { + return { + state: "not found" + }; + } + + const expirationDate = await whois(domain); + const result = { + state: expirationDate === null ? "expired" : "active", + expirationDate, + mxRecords: mxRecords.safeUnwrap() + }; + DOMAINS_CACHE.set(domain, result); + + return result; +} + +async function safeResolveMx(domain) { + try { + const mxRecords = await dns.resolveMx(domain); + + return Some( + mxRecords.map(({ exchange }) => exchange) + ); + } + catch { + return None; + } +} diff --git a/src/helper.js b/src/helper.js deleted file mode 100644 index 8d0706e..0000000 --- a/src/helper.js +++ /dev/null @@ -1,9 +0,0 @@ -const domainsInformations = {}; - -export function storeDomainExpirationInMemory({ domain, expirationDate }) { - domainsInformations[domain] = expirationDate; -} - -export function getDomainExpirationFromMemory(domain) { - return domainsInformations[domain] ?? undefined; -} diff --git a/src/whois.js b/src/whois.js index fdb243a..5fa4818 100644 --- a/src/whois.js +++ b/src/whois.js @@ -23,19 +23,22 @@ function* lazyParseIanaWhoisResponse(rawResponseStr) { export async function whois(domain, server = kDefaultSocketServer) { const client = new net.Socket(); client.setTimeout(1_000); - setImmediate(() => client.connect(43, server, () => client.write(`${domain}\r\n`))); + client.connect(43, server, () => client.write(`${domain}\r\n`)); try { const rawResponseStr = await streamConsumers.text(client); - const response = Object.fromEntries(lazyParseIanaWhoisResponse(rawResponseStr)); + const response = Object.fromEntries( + lazyParseIanaWhoisResponse(rawResponseStr) + ); if ("refer" in response && response.refer !== server) { return whois(domain, response.refer); } - return response["Registry Expiry Date"]; + return response?.["Registry Expiry Date"] ?? null; } finally { client.destroy(); } } + diff --git a/test/getDomainInformations.spec.js b/test/getDomainInformations.spec.js new file mode 100644 index 0000000..024e9e6 --- /dev/null +++ b/test/getDomainInformations.spec.js @@ -0,0 +1,30 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { getDomainInformations } from "../src/domains.js"; + +function isValidDate(dateString) { + return dateString.match(/^\d{4}-\d{2}-\d{2}/) !== null; +} + +describe("getDomainInformations", () => { + it("should fetch/get domain informations for google.com", async() => { + const domain = "google.com"; + + const infos = await getDomainInformations(domain); + + assert.strict(infos.state, "active"); + assert.ok(isValidDate(infos.expirationDate)); + assert.ok(Array.isArray(infos.mxRecords)); + assert.strictEqual(infos.mxRecords[0], "smtp.google.com"); + }); + + it("should return state equal 'not found' for an unknown domain", async() => { + const domain = "zbvoepokxfkxlzkejnckekjx.ru"; + + const infos = await getDomainInformations(domain); + assert.strict(infos.state, "not found"); + }); +}); diff --git a/test/whois.spec.js b/test/whois.spec.js new file mode 100644 index 0000000..471d7ba --- /dev/null +++ b/test/whois.spec.js @@ -0,0 +1,23 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { whois } from "../src/whois.js"; + +function isValidDate(dateString) { + return dateString.match(/^\d{4}-\d{2}-\d{2}/) !== null; +} + +describe("whois", () => { + it("should resolve domain expiration date for 'google.com'", async() => { + // Given + const domain = "google.com"; + + // When + const domainExpirationDate = await whois(domain); + + // Then + assert.ok(isValidDate(domainExpirationDate)); + }); +}); From be0c0de47b9260e6198b66268ffb54163c8f9449 Mon Sep 17 00:00:00 2001 From: fraxken Date: Sun, 26 Nov 2023 16:35:11 +0100 Subject: [PATCH 4/4] chore: stage work --- index.d.ts | 34 ------- index.js | 89 ------------------ package.json | 13 ++- src/domains.js | 47 ---------- src/domains.ts | 66 +++++++++++++ src/index.ts | 122 +++++++++++++++++++++++++ src/{levenshtein.js => levenshtein.ts} | 36 +++++--- src/utils.js | 51 ----------- src/utils.ts | 86 +++++++++++++++++ src/{whois.js => utils/whois.ts} | 10 +- tsconfig.json | 20 ++++ 11 files changed, 333 insertions(+), 241 deletions(-) delete mode 100644 index.d.ts delete mode 100644 index.js delete mode 100644 src/domains.js create mode 100644 src/domains.ts create mode 100644 src/index.ts rename src/{levenshtein.js => levenshtein.ts} (70%) delete mode 100644 src/utils.js create mode 100644 src/utils.ts rename src/{whois.js => utils/whois.ts} (82%) create mode 100644 tsconfig.json diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index c428afc..0000000 --- a/index.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Import Third-party Dependencies -import { Scanner } from "@nodesecure/scanner"; - -export function extractAllAuthors(library: Scanner.Payload, opts: options): Promise - -export interface options { - flaggedAuthors: extractedAuthor[], - domainInformations: boolean, -} - -export interface extractionResult { - authors: author[], - flaggedAuthors: extractedAuthor[], -} - -export interface author { - name?: string; - email?: string; - url?: string; - packages: { - homepage: string, - spec: string, - version: string, - at?: string, - }[], - domain?: { - expirationDate?: string, - mxRecords?: unknown[], - } -} -export interface extractedAuthor { - name: string, - email: string, -} diff --git a/index.js b/index.js deleted file mode 100644 index a20e291..0000000 --- a/index.js +++ /dev/null @@ -1,89 +0,0 @@ -// Import Internal Dependencies -import { getDomainInformations } from "./src/domains.js"; -import { useLevenshtein } from "./src/levenshtein.js"; -import { getDomainExpirationFromMemory, storeDomainExpirationInMemory } from "./src/helper.js"; -import * as utils from "./src/utils.js"; - -export async function extractAllAuthors( - library, - opts = { flaggedAuthors: [], domainInformations: false } -) { - if (!("dependencies" in library)) { - throw new Error("You must provide a list of dependencies"); - } - - const authors = []; - for (let index = 0; index < Object.values(library.dependencies).length; index++) { - const currPackage = { - packageName: Object.keys(library.dependencies)[index], - ...Object.values(library.dependencies)[index] - }; - - const { author, maintainers, publishers } = currPackage.metadata; - const packageMeta = { - homepage: currPackage.metadata.homepage || "", - spec: currPackage.packageName, - versions: currPackage.metadata.lastVersion - }; - - const authorsFound = utils.formatAuthors({ author, maintainers, publishers }); - - for (const author of authorsFound) { - if (author === undefined) { - continue; - } - authors.push({ - name: author.name, - email: author.email, - packages: [{ - ...packageMeta, - ...(author?.at ? { at: author.at } : {}) - }] - }); - } - } - if (authors.length === 0) { - return { - authorsFlagged: [], - authors: [] - }; - } - - const authorsFlagged = Array.from(findFlaggedAuthors( - useLevenshtein(authors), - opts.flaggedAuthors - )); - if (opts.domainInformations) { - await addDomainInformations(authors); - } - - return { - authorsFlagged, - authors - }; -} - -async function addDomainInformations(authors) { - for (const author of authors) { - if (author.email === "") { - continue; - } - const domain = author.email.split("@")[1]; - - if (domain) { - author.domain = await getDomainInformations(domain); - } - } - - return authors; -} - -function* findFlaggedAuthors(authors, flaggedAuthors = []) { - for (const author of authors) { - for (const flaggedAuthor of flaggedAuthors) { - if (flaggedAuthor.name === author.name || flaggedAuthor.email === author.email) { - yield { name: author.name, email: author.email }; - } - } - } -} diff --git a/package.json b/package.json index 34f957c..dd98b83 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,12 @@ "name": "@nodesecure/authors", "version": "1.0.2", "description": "NodeSecure (npm) authors analysis package", - "exports": "./index.js", + "exports": "./dist/index.js", "type": "module", - "types": "./index.d.ts", + "types": "./dist/index.d.ts", "scripts": { - "lint": "cross-env eslint ./src", + "build": "tsc", + "lint": "eslint src/*.ts", "test-only": "node --test test/", "test": "npm run lint && npm run test-only", "coverage": "c8 -r html npm test" @@ -35,11 +36,13 @@ "devDependencies": { "@nodesecure/eslint-config": "^1.8.0", "@nodesecure/scanner": "^4.0.0", - "@npm/types": "^1.0.2", - "c8": "^8.0.1" + "@types/node": "^20.5.9", + "c8": "^8.0.1", + "typescript": "^5.2.2" }, "dependencies": { "@myunisoft/httpie": "^2.0.1", + "@npm/types": "^1.0.2", "@openally/result": "^1.2.0" } } diff --git a/src/domains.js b/src/domains.js deleted file mode 100644 index d04dd1d..0000000 --- a/src/domains.js +++ /dev/null @@ -1,47 +0,0 @@ -// Import Node.js Dependencies -import dns from "node:dns/promises"; - -// Import Third-party Dependencies -import { Some, None } from "@openally/result"; - -// Import Internal Dependencies -import { whois } from "./whois.js"; - -// VARS -export const DOMAINS_CACHE = new Map(); - -export async function getDomainInformations(domain) { - if (DOMAINS_CACHE.has(domain)) { - return DOMAINS_CACHE.get(domain); - } - - const mxRecords = await safeResolveMx(domain); - if (mxRecords.none) { - return { - state: "not found" - }; - } - - const expirationDate = await whois(domain); - const result = { - state: expirationDate === null ? "expired" : "active", - expirationDate, - mxRecords: mxRecords.safeUnwrap() - }; - DOMAINS_CACHE.set(domain, result); - - return result; -} - -async function safeResolveMx(domain) { - try { - const mxRecords = await dns.resolveMx(domain); - - return Some( - mxRecords.map(({ exchange }) => exchange) - ); - } - catch { - return None; - } -} diff --git a/src/domains.ts b/src/domains.ts new file mode 100644 index 0000000..60cb00e --- /dev/null +++ b/src/domains.ts @@ -0,0 +1,66 @@ +// Import Node.js Dependencies +import dns from "node:dns/promises"; + +// Import Third-party Dependencies +import { Some, None, Option } from "@openally/result"; + +// Import Internal Dependencies +import { whois } from "./utils/whois.js"; + +export type DnsNotFound = { + state: "not found"; +} + +export type DnsResolved = { state: "not found" } | { + state: "expired" | "active"; + expirationDate: string | null; + mxRecords: string[]; +} + +export type ResolveResult = DnsNotFound | DnsResolved; + +export class DomainResolver { + #cache: Map = new Map(); + + async resolve( + domain: string + ): Promise { + if (this.#cache.has(domain)) { + return this.#cache.get(domain)!; + } + + const mxRecords = await safeResolveMx(domain); + if (mxRecords.none) { + return { + state: "not found" + }; + } + + const expirationDate = await whois(domain); + const result: DnsResolved = { + state: expirationDate === null ? "expired" : "active", + expirationDate, + mxRecords: mxRecords.safeUnwrap() + }; + this.#cache.set(domain, result); + + return result; + } + + clear() { + this.#cache.clear(); + } +} + +async function safeResolveMx(domain: string): Promise> { + try { + const mxRecords = await dns.resolveMx(domain); + + return Some( + mxRecords.map(({ exchange }) => exchange) + ); + } + catch { + return None; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..73c2bd5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,122 @@ +// Import Third-party Dependencies +import { Scanner } from "@nodesecure/scanner"; + +// Import Internal Dependencies +// import { DomainResolver } from "./domains.js"; +// import { useLevenshtein } from "./levenshtein.js"; +import * as utils from "./utils.js"; + +export interface ExtractedAuthor { + name: string; + email?: string; +} + +export interface ExtractOptions { + flaggedAuthors: ExtractedAuthor[], + domainInformations: boolean, +} + +export interface ExtractResult { + authors: RegistryAuthor[], + flaggedAuthors: ExtractedAuthor[], +} + +export interface RegistryAuthor { + name: string; + email?: string; + url?: string; + packages: { + homepage: string, + spec: string, + version?: string, + at?: string, + }[], + domain?: { + expirationDate?: string, + mxRecords?: unknown[], + } +} + +export async function extractAllAuthors( + library: Scanner.Payload, + opts: ExtractOptions = { flaggedAuthors: [], domainInformations: false } +): Promise { + if (!("dependencies" in library)) { + throw new Error("You must provide a list of dependencies"); + } + + const authors: RegistryAuthor[] = []; + for (const [packageName, packageDependency] of Object.entries(library.dependencies)) { + const { author, maintainers, publishers } = packageDependency.metadata; + + const authorsFound = utils.formatAuthors({ + author, maintainers, publishers + }); + const packageMeta = { + homepage: packageDependency.metadata.homepage ?? "", + spec: packageName, + versions: packageDependency.metadata.lastVersion + }; + + for (const author of authorsFound) { + authors.push({ + name: author.name, + email: author.email, + packages: [{ + ...packageMeta, + ...(author?.at ? { at: author.at } : {}) + }] + }); + } + } + console.log(JSON.stringify(authors, null, 2)); + + // if (authors.length === 0) { + // return { + // flaggedAuthors: [], + // authors: [] + // }; + // } + + // const flaggedAuthors = Array.from(findFlaggedAuthors( + // useLevenshtein(authors), + // opts.flaggedAuthors + // )); + // if (opts.domainInformations) { + // await addDomainInformations(authors); + // } + + // return { + // flaggedAuthors, + // authors + // }; +} + +// async function addDomainInformations(authors) { +// const domainResolver = new DomainResolver(); +// for (const author of authors) { +// if (author.email === "") { +// continue; +// } +// const domain = author.email.split("@")[1]; + +// if (domain) { +// author.domain = await domainResolver.resolve(domain); +// } +// } + +// return authors; +// } + +// function* findFlaggedAuthors( +// authors: RegistryAuthor[], +// flaggedAuthors: ExtractedAuthor[] = [] +// ): IterableIterator<{ name: string, email: string }> { +// for (const author of authors) { +// for (const flaggedAuthor of flaggedAuthors) { +// if (flaggedAuthor.name === author.name || flaggedAuthor.email === author.email) { +// yield { name: author.name, email: author.email }; +// } +// } +// } +// } diff --git a/src/levenshtein.js b/src/levenshtein.ts similarity index 70% rename from src/levenshtein.js rename to src/levenshtein.ts index fa29073..cc977d5 100644 --- a/src/levenshtein.js +++ b/src/levenshtein.ts @@ -1,7 +1,9 @@ +import { FormattedAuthor } from "./utils"; + // CONSTANTS const kMaxLevenshteinDistance = 2; -export function separateWord(word) { +export function separateWord(word: string): string | string[] { const separators = [",", " ", "."]; let separatorFound = ""; let indexSeparator = -1; @@ -17,7 +19,11 @@ export function separateWord(word) { return indexSeparator === -1 ? word : word.split(separatorFound); } -export function isSimilar(firstWord, secondWord, isWordSeparated = false) { +export function isSimilar( + firstWord: string | undefined, + secondWord: string | undefined, + isWordSeparated = false +) { if (!firstWord || !secondWord) { return kMaxLevenshteinDistance; } @@ -29,9 +35,11 @@ export function isSimilar(firstWord, secondWord, isWordSeparated = false) { const word2Splitted = separateWord(word2); const firstWord = isSimilar( + // @ts-ignore word1Splitted instanceof Array ? word1Splitted[0] : null, word2Splitted instanceof Array ? word2Splitted[0] : null); const secondWord = isSimilar( + // @ts-ignore word1Splitted instanceof Array ? word1Splitted[1] : null, word2Splitted instanceof Array ? word2Splitted[1] : null ); @@ -64,22 +72,26 @@ export function isSimilar(firstWord, secondWord, isWordSeparated = false) { return track[word2.length][word1.length]; } -export function useLevenshtein(authors) { +export function useLevenshtein( + authors: FormattedAuthor[] +): FormattedAuthor[] { const authorsResponse = [authors[0]]; iterationAuthor: - for (let index = 1; index < authors.length; index++) { - const currAuthor = authors[index]; - + for (const currAuthor of authors) { for (const author of authorsResponse) { - if (isSimilar(author.email, currAuthor.email) < kMaxLevenshteinDistance - || isSimilar(author.name, currAuthor.name, true) < kMaxLevenshteinDistance) { - author.email = author.email.length > currAuthor.email.length ? author.email : currAuthor.email; - author.name = author.name.length > currAuthor.name.length ? author.name : currAuthor.name; + const hasSimilarEmail = isSimilar(author.email, currAuthor.email) < kMaxLevenshteinDistance; + const hasSimilarName = isSimilar(author.name, currAuthor.name, true) < kMaxLevenshteinDistance; - if ("packages" in currAuthor) { - author.packages.push(...currAuthor.packages); + if (hasSimilarEmail || hasSimilarName) { + if (author.email && currAuthor.email) { + author.email = author.email.length > currAuthor.email.length ? author.email : currAuthor.email; } + author.name = author.name.length > currAuthor.name.length ? author.name : currAuthor.name; + + // if ("packages" in currAuthor) { + // author.packages.push(...currAuthor.packages); + // } continue iterationAuthor; } } diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 1ba38a3..0000000 --- a/src/utils.js +++ /dev/null @@ -1,51 +0,0 @@ -// Import Internal Dependencies -import { useLevenshtein } from "./levenshtein.js"; - -export function formatAuthors({ author, maintainers, publishers }) { - const authors = []; - - if (author?.name !== undefined) { - authors.push(splitAuthorNameEmail(author)); - } - iterateOver(maintainers, authors); - iterateOver(publishers, authors); - - return useLevenshtein(authors); -} - -export function iterateOver(iterable, arrayAuthors) { - for (const contributor of iterable) { - if (arrayAuthors.find((el) => el.name === contributor.name)) { - const index = arrayAuthors.findIndex((el) => el.name === contributor.name); - - if (arrayAuthors[index].email && arrayAuthors[index].name) { - if (contributor.at && contributor.version) { - arrayAuthors[index].at = contributor.at; - arrayAuthors[index].version = contributor.version; - } - continue; - } - arrayAuthors[index] = contributor; - } - else { - arrayAuthors.push(contributor); - } - } -} - -export function splitAuthorNameEmail(author) { - const indexStartEmail = author.name.search(/[<]/g); - const indexEndEmail = author.name.search(/[>]/g); - - if (indexStartEmail === -1 && indexEndEmail === -1) { - return { - name: author.name, - email: "email" in author ? author.email : "" - }; - } - - return { - name: author.substring(0, indexStartEmail).trim(), - email: author.substring(indexStartEmail, indexEndEmail).trim() - }; -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5a0768e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,86 @@ +// // Import Internal Dependencies +import { useLevenshtein } from "./levenshtein.js"; + +// Import Third-party Dependencies +import { Maintainer } from "@npm/types"; +import { Scanner } from "@nodesecure/scanner"; + +type MaintainerBis = { name: string, email: string }; + +export interface FormatAuthorsOptions { + author: Maintainer; + maintainers: MaintainerBis[]; + publishers: Scanner.Publisher[]; +} + +export interface FormattedAuthor { + name: string; + email?: string; + version?: string; + at?: string; +} + +export function formatAuthors( + options: FormatAuthorsOptions +) { + const { author, maintainers, publishers } = options; + + const authors: FormattedAuthor[] = []; + const extractedAuthor = splitAuthorNameEmail(author); + if (extractedAuthor !== null) { + authors.push(extractedAuthor); + } + + iterateOver(maintainers, authors); + iterateOver(publishers, authors); + + return useLevenshtein(authors); +} + +export function iterateOver( + iterable: (MaintainerBis | Scanner.Publisher)[], + arrayAuthors: FormattedAuthor[] +) { + for (const contributor of iterable) { + const index = arrayAuthors.findIndex((el) => el.name === contributor.name); + + if (index === -1) { + arrayAuthors.push(contributor); + continue; + } + + const currAuthor = arrayAuthors[index]; + if ( + currAuthor.email && currAuthor.name && + ("at" in contributor && "version" in contributor) + ) { + currAuthor.at = contributor.at; + currAuthor.version = contributor.version; + } + else { + arrayAuthors[index] = contributor; + } + } +} + +export function splitAuthorNameEmail( + author: Maintainer +): FormattedAuthor | null { + if (typeof author === "string") { + const indexStartEmail = author.search(/[<]/g); + const indexEndEmail = author.search(/[>]/g); + + if (indexStartEmail === -1 && indexEndEmail === -1) { + return { name: author }; + } + + return { + name: author.slice(0, indexStartEmail).trim(), + email: author.slice(indexStartEmail, indexEndEmail).trim() + }; + } + + return "name" in author ? + { name: author.name!, email: author.email! } : + null; +} diff --git a/src/whois.js b/src/utils/whois.ts similarity index 82% rename from src/whois.js rename to src/utils/whois.ts index 5fa4818..0586c2c 100644 --- a/src/whois.js +++ b/src/utils/whois.ts @@ -5,8 +5,9 @@ import streamConsumers from "node:stream/consumers"; // CONSTANTS const kDefaultSocketServer = "whois.iana.org"; -function* lazyParseIanaWhoisResponse(rawResponseStr) { - /** @type {string[]} */ +function* lazyParseIanaWhoisResponse( + rawResponseStr: string +): IterableIterator<[string, string]> { const lines = rawResponseStr.split(/\r?\n/); for (const line of lines) { @@ -20,7 +21,10 @@ function* lazyParseIanaWhoisResponse(rawResponseStr) { } } -export async function whois(domain, server = kDefaultSocketServer) { +export async function whois( + domain: string, + server = kDefaultSocketServer +): Promise { const client = new net.Socket(); client.setTimeout(1_000); client.connect(43, server, () => client.write(`${domain}\r\n`)); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3265b53 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "declaration": true, + "strictNullChecks": true, + "target": "ES2022", + "outDir": "dist", + "module": "ES2020", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "skipDefaultLibCheck": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true, + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src", "src/schema/**/*.json"], + "exclude": ["node_modules", "dist"] +}