From 0c22a3f44ff25ac95e8080f27c3dce35c3b5b060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 21:26:40 +0200 Subject: [PATCH 01/22] port config parsers --- src/config.parsers.mjs | 161 --------------------------------------- src/config.parsers.ts | 167 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 161 deletions(-) delete mode 100644 src/config.parsers.mjs create mode 100644 src/config.parsers.ts diff --git a/src/config.parsers.mjs b/src/config.parsers.mjs deleted file mode 100644 index d3d3085..0000000 --- a/src/config.parsers.mjs +++ /dev/null @@ -1,161 +0,0 @@ -import assert from 'node:assert' - -/** - * Parse config value as boolean. - * @param {any} value Value - * @returns {boolean?} Boolean or undefined - */ -export function boolean (value) { - return value !== undefined - ? value === 'true' - : undefined -} - -/** - * Parse config value as integer. - * - * @param {any} value Value - * @returns {number?} Integer or undefined - */ -export function integer (value) { - const result = parseInt(value) - return isNaN(result) ? undefined : result -} - -/** -* Parse config value as number -* -* @param {any} value Value -* @returns {number?} Number or undefined -*/ -export function number (value) { - const result = parseFloat(value) - return isNaN(result) ? undefined : result -} - -/** - * Parse config value as enum. - * - * @param {any} value Value - * @param {Array} values Allowed values - * @returns {any?} Allowed value or undefined - */ -export function enumerated (value, values) { - return values.includes(value) - ? value - : undefined -} - -/** -* Split an input into nominator and unit -* @param {string} value -* @returns {number[]} -*/ -function extractUnit (value) { - const pattern = /^([0-9.,]+)([a-zA-Z]*)/ - - const groups = pattern.exec(value) - assert(groups, `Can't parse input "${value}"`) - - return [groups[1], groups[2]] -} - -/** -* Parse config value as human-readable size -* -* @param {any} value Value -* @returns {number?} Number or undefined -*/ -export function byteSize (value) { - if (value === undefined) { - return value - } - - const postfixes = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] - - const [nominator, unit] = extractUnit(value) - - const idx = postfixes.findIndex(pf => pf === (unit || 'b').toLowerCase()) - assert(idx >= 0, `Unknown byte postfix "${unit}"!`) - - return number(nominator) * Math.pow(1024, idx) -} - -/** -* Parse config value as human-readable duration -* -* @param {any} value Value -* @returns {number?} Number or undefined -*/ -export function duration (value) { - if (value === undefined) { - return value - } - - const units = { - '': 1, - us: 0.000001, - ms: 0.001, - s: 1, - m: 60, - h: 3600, - hr: 3600, - d: 86400, - w: 604800, - mo: 2592000, - yr: 31536000 - } - - const [nominator, unit] = extractUnit(value.toLowerCase()) - assert(units[unit], `Unknown duration unit "${unit}"!`) - - return number(nominator) * units[unit] -} - -/** -* Parse config value as port ranges. -* -* Three kinds of port ranges are parsed: -* 1. literal - e.g. '1007' becomes [1007] -* 1. absolute - e.g. '1024-1026' becomes [1024, 1025, 1026] -* 1. relative - e.g. '1024+2' becomes [1024, 1025, 1026] -* -* These ranges are separated by a comma, e.g.: -* `1007, 1024-1026, 2048+2` -* -* @param {string} value Value -* @returns {number[]} Ports -*/ -export function ports (value) { - if (value === undefined) { - return undefined - } - - const ranges = value.split(',').map(r => r.trim()) - - const literals = ranges - .filter(p => /^\d+$/.test(p)) - .map(integer) - .filter(p => !!p) - .map(p => [p, p]) - - const absolutes = ranges - .filter(r => r.includes('-')) - .map(r => r.split('-').map(integer)) - .filter(r => !r.includes(undefined)) - - const relatives = ranges - .filter(r => r.includes('+')) - .map(r => r.split('+').map(integer)) - .filter(r => !r.includes(undefined)) - .map(([from, offset]) => [from, from + offset]) - - const result = [...literals, ...absolutes, ...relatives] - .flatMap(([from, to]) => - [...new Array(to - from + 1)].map((_, i) => from + i) - ) - .sort() - .filter((v, i, a) => i === 0 || v !== a[i - 1]) // ensure every port is unique - - return result.length > 0 ? result : undefined -} diff --git a/src/config.parsers.ts b/src/config.parsers.ts new file mode 100644 index 0000000..92839ff --- /dev/null +++ b/src/config.parsers.ts @@ -0,0 +1,167 @@ +import assert from "node:assert"; + +/** + * Parse config value as boolean. + */ +export function boolean(value: string | undefined): boolean | undefined { + return value !== undefined ? value === "true" : undefined; +} + +/** + * Parse config value as integer. + */ +export function integer(value: string | undefined): number | undefined { + const result = parseInt(value ?? "", 10); + return Number.isNaN(result) ? undefined : result; +} + +/** + * Parse config value as number + */ +export function number(value: string | undefined): number | undefined { + const result = parseFloat(value ?? ""); + return Number.isNaN(result) ? undefined : result; +} + +/** + * Parse config value as enum. + * + * @param value Value + * @param values Allowed values + * @returns Allowed value or undefined + */ +export function enumerated(value: T, values: T[]): T | undefined { + return values.includes(value) ? value : undefined; +} + +/** + * Split an input into nominator and unit + */ +function extractUnit(value: string): [string, string] { + const pattern = /^([0-9.,]+)([a-zA-Z]*)/; + + const groups = pattern.exec(value); + assert(groups, `Can't parse input "${value}"`); + + return [groups[1], groups[2]]; +} + +/** + * Parse config value as human-readable size + */ +export function byteSize(value: string | undefined): number | undefined { + if (value === undefined) { + return value; + } + + const postfixes = [ + "b", + "kb", + "mb", + "gb", + "tb", + "pb", + "eb", + "zb", + "yb", + "bps", + "kbps", + "mbps", + "gbps", + "tbps", + "pbps", + "ebps", + "zbps", + "ybps", + ]; + + const [nominator, unit] = extractUnit(value); + const nominated = number(nominator); + + const idx = postfixes.findIndex((pf) => pf === (unit || "b").toLowerCase()); + assert(idx >= 0, `Unknown byte postfix "${unit}"!`); + + return nominated !== undefined ? nominated * 1024 ** idx : undefined; +} + +/** + * Parse config value as human-readable duration + */ +export function duration(value: string | undefined): number | undefined { + if (value === undefined) { + return value; + } + + const units: Record = { + "": 1, + us: 0.000001, + ms: 0.001, + s: 1, + m: 60, + h: 3600, + hr: 3600, + d: 86400, + w: 604800, + mo: 2592000, + yr: 31536000, + }; + + const [nominator, unit] = extractUnit(value.toLowerCase()); + const nominated = number(nominator); + assert(units[unit], `Unknown duration unit "${unit}"!`); + + return nominated !== undefined ? nominated * units[unit] : undefined; +} + +/** + * Parse config value as port ranges. + * + * Three kinds of port ranges are parsed: + * 1. literal - e.g. '1007' becomes [1007] + * 2. absolute - e.g. '1024-1026' becomes [1024, 1025, 1026] + * 3. relative - e.g. '1024+2' becomes [1024, 1025, 1026] + * + * These ranges are separated by a comma, e.g.: + * `1007, 1024-1026, 2048+2` + * + * @param value Value + * @returns Ports + */ +export function ports(value: string | undefined): number[] | undefined { + if (value === undefined) { + return undefined; + } + + const ranges = value.split(",").map((r) => r.trim()); + + const literals = ranges + .filter((p) => /^\d+$/.test(p)) + .map(integer) + .filter((p) => p !== undefined) + .map((p) => [p, p]); + + const absolutes: Array<[number, number]> = ranges + .filter((r) => r.includes("-")) + .map((r) => r.split("-")) + .map(([from, to]) => [integer(from), integer(to)] as [number, number]) + .filter(([from, to]) => from !== undefined && to !== undefined); + + const relatives = ranges + .filter((r) => r.includes("+")) + .map((r) => r.split("+").map(integer)) + .map(([from, offset]) => + from !== undefined && offset !== undefined + ? [from, from + offset] + : undefined, + ) + .filter((it) => it !== undefined); + + const result = [...literals, ...absolutes, ...relatives] + .flatMap(([from, to]) => + [...new Array(to - from + 1)].map((_, i) => from + i), + ) + .sort() + .filter((v, i, a) => i === 0 || v !== a[i - 1]); // ensure every port is unique + + return result.length > 0 ? result : undefined; +} From 162d281e5664a062793d8e74df80df7a3fb5fc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 21:40:29 +0200 Subject: [PATCH 02/22] port more --- src/config.parsers.ts | 2 +- src/logger.mjs | 20 - src/logger.ts | 26 + src/utils.mjs | 219 ---- src/utils.ts | 199 ++++ src/wordlist.mjs | 2640 ----------------------------------------- src/wordlist.ts | 2640 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 2866 insertions(+), 2880 deletions(-) delete mode 100644 src/logger.mjs create mode 100644 src/logger.ts delete mode 100644 src/utils.mjs create mode 100644 src/utils.ts delete mode 100644 src/wordlist.mjs create mode 100644 src/wordlist.ts diff --git a/src/config.parsers.ts b/src/config.parsers.ts index 92839ff..9c83faa 100644 --- a/src/config.parsers.ts +++ b/src/config.parsers.ts @@ -30,7 +30,7 @@ export function number(value: string | undefined): number | undefined { * @param values Allowed values * @returns Allowed value or undefined */ -export function enumerated(value: T, values: T[]): T | undefined { +export function enumerated(value: T, values: readonly T[]): T | undefined { return values.includes(value) ? value : undefined; } diff --git a/src/logger.mjs b/src/logger.mjs deleted file mode 100644 index 4ce5806..0000000 --- a/src/logger.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import pino from 'pino' -import * as dotenv from 'dotenv' -import { enumerated } from './config.parsers.mjs' - -export const loglevels = Object.freeze([ - 'silent', 'trace', 'debug', 'info', 'warn', 'error', 'fatal' -]) - -dotenv.config() - -export function getLogLevel () { - return enumerated(process.env.NORAY_LOGLEVEL, loglevels) ?? 'info' -} - -const logger = pino({ - name: 'noray', - level: getLogLevel() -}) - -export default logger diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..3eb003e --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,26 @@ +import pino from "pino"; +import * as dotenv from "dotenv"; +import { enumerated } from "./config.parsers"; + +export const loglevels = Object.freeze([ + "silent", + "trace", + "debug", + "info", + "warn", + "error", + "fatal", +]); + +dotenv.config(); + +export function getLogLevel(): string { + return enumerated(process.env.NORAY_LOGLEVEL, loglevels) ?? "info"; +} + +const logger = pino({ + name: "noray", + level: getLogLevel(), +}); + +export default logger; diff --git a/src/utils.mjs b/src/utils.mjs deleted file mode 100644 index c3c2fed..0000000 --- a/src/utils.mjs +++ /dev/null @@ -1,219 +0,0 @@ -import { randomInt } from 'node:crypto' -import words from './wordlist.mjs' - -/** -* Return current time ( seconds since epoch ). -* @returns {number} -*/ -export function time () { - return (performance.now() + performance.timeOrigin) / 1000 -} - -/** -* Sleep. -* @param {number} seconds Time to sleep in seconds -* @param {T} [value=undefined] Value to resolve to -* @returns {Promise} Promise -* @template T -*/ -export function sleep (seconds, value) { - return new Promise(resolve => setTimeout(() => resolve(value), seconds * 1000)) -} - -/** -* Wait for an event on event source. -* @param {any} source Event source -* @param {string} event Event name -* @returns {Promise} Promise -*/ -export function promiseEvent (source, event) { - return new Promise(resolve => { - source.on(event, resolve) - }) -} - -/** -* Wrap a function as a singleton factory. -* -* In practice, this will create a function that will cache the wrapped -* function's return value, assuming it always returns the same thing. -* -* NOTE: This will evaluate the method while wrapping. -* -* @param {function(): T} f Function to wrap -* @returns {function(): T} Singleton factory function -* @template T -*/ -export function asSingletonFactory (f) { - const value = f() - return () => value -} - -/** -* Memoize function. -* -* That is, for every set of input arguments, remember the result. The next time -* the same arguments are used, instead of calculating the result again, it will -* be recovered from cache. -* -* **NOTE** that the cache is not limited in any way, use only in cases where -* the possible number of parameters is limited. -* -* @param {function(): T} f Function -* @returns {function(): T} Memoized function -* @template T -*/ -export function memoize (f) { - const cache = new Map() - return function () { - const key = JSON.stringify(arguments) - - if (!cache.has(key)) { - cache.set(key, f(...arguments)) - } - - return cache.get(key) - } -} - -/** -* Maps an input array into chunks of a given size. The last chunk might be -* smaller than the requested size. -* -* @param {T[]} data Data -* @param {number} size Chunk size -* @returns {T[][]} An array of chunks -* @template T -*/ -export function chunks (data, size) { - const count = Math.ceil(data.length / size) - return [...new Array(count)] - .map((_, i) => data.slice(i * size, (i + 1) * size)) -} - -/** -* Symbol for timeout. -*/ -export const Timeout = Symbol('timeout') - -/** -* Limit a promise run to a given timeout. -* -* If the promise doesn't resolve in time, the Timeout symbol will be returned. -* Otherwise, the promise's result will be passed through. -* @param {Promise} promise Promise -* @param {number} timeout Timeout in seconds -* @returns {Promise} Promise result or Timeout symbol -* @template T -*/ -export function withTimeout (promise, timeout) { - return Promise.race([ - sleep(timeout, Timeout), - promise - ]) -} - -/** -* Generate an array of numbers 0..n -* @param {number} n Item count -* @returns {number[]} Numbers -*/ -export function range (n) { - return [...new Array(Math.max(~~n, 0))] - .map((_, i) => i) -} - -/** -* Generate all possible combinations of the values in the given arrays. -* -* For example: -* ```js -* console.log(['a', 'b'], [0, 1], [true, false]) -* // ['a', 0, true] -* // ['a', 0, false] -* // ['a', 1, true] -* // ['a', 1, false] -* // ['b', 0, true] -* // ['b', 0, false] -* // ['b', 1, true] -* // ['b', 1, false] -* ``` -* @param {...Array} arrays Arrays to combine -* @returns {Array>} Array of combinations -* @template T -*/ -export function combine (...arrays) { - const count = arrays.map(a => a.length) - .reduce((a, b) => a * b, 1) - const dimensions = arrays.length - - const result = range(count) - - for (let i = 0; i < count; ++i) { - const item = new Array(dimensions) - let idx = i - - for (let d = 0; d < dimensions; ++d) { - const divisor = arrays[d].length - item[d] = arrays[d][idx % divisor] - idx = ~~(idx / divisor) - } - - result[i] = item - } - - return result -} - -/** -* Format a size in bytes to a human readable form. -* -* For example, 131072 becomes 128kb. -* @param {number} size Size in bytes -* @returns {string} Human readable size -*/ -export function formatByteSize (size) { - const postfixes = ['b', 'kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb'] - const pfi = (postfixes.length + postfixes.findIndex((_, i) => size < Math.pow(1024, i + 1))) % - postfixes.length - - return (size / Math.pow(1024, pfi)) + postfixes[pfi] -} - -/** -* Format a duration to a human readable form. -* -* For example, 720 becomes 12min. -* @param {number} seconds Duration in seconds -* @returns {string} Human readable duration -*/ -export function formatDuration (seconds) { - const units = Object.entries({ - us: 0.000001, - ms: 0.001, - sec: 1, - min: 60, - hr: 3600, - day: 86400, - wk: 604800, - mo: 2592000, - yr: 31536000 - }).reverse() - - const [unit, multiplier] = units.find(([_, f]) => seconds > f) ?? units.at(-1) - - return (seconds / multiplier) + unit -} - -/** -* Select random words from wordlist. -* -* Used to generate a word based OID For example, FalconTimberYolk -* @param {integer} Word count -* @returns {string} Concatenated words -*/ -export function generateWordId (wordCount = 3) { - return range(wordCount) - .map(() => words[randomInt(words.length)]) - .join('') -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..cae5199 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,199 @@ +import { randomInt } from "node:crypto"; +import { type EventEmitter } from "node:events"; +import words from "./wordlist"; + +/** + * Return current time ( seconds since epoch ). + */ +export function time(): number { + return (performance.now() + performance.timeOrigin) / 1000; +} + +/** + * Sleep. + */ +export function sleep(seconds: number, value: T): Promise { + return new Promise((resolve) => + setTimeout(() => resolve(value), seconds * 1000), + ); +} + +/** + * Wait for an event on event source. + */ +export function promiseEvent( + source: EventEmitter, + event: string, +): Promise { + return new Promise((resolve) => { + source.on(event, resolve); + }); +} + +/** + * Wrap a function as a singleton factory. + * + * In practice, this will create a function that will cache the wrapped + * function's return value, assuming it always returns the same thing. + * + * NOTE: This will evaluate the method while wrapping. + */ +export function asSingletonFactory(f: () => T): () => T { + const value = f(); + return () => value; +} + +/** + * Memoize function. + * + * That is, for every set of input arguments, remember the result. The next time + * the same arguments are used, instead of calculating the result again, it will + * be recovered from cache. + * + * NOTE: The cache is not limited in any way, use only in cases where + * the possible number of parameters is limited. + */ +export function memoize(f: Function): Function { + const cache = new Map(); + return function() { + const key = JSON.stringify(arguments); + + if (!cache.has(key)) { + cache.set(key, f(...arguments)); + } + + return cache.get(key); + }; +} + +/** + * Maps an input array into chunks of a given size. The last chunk might be + * smaller than the requested size. + * + * @param {T[]} data Data + * @param {number} size Chunk size + * @returns {T[][]} An array of chunks + * @template T + */ +export function chunks(data: T[], size: number): T[][] { + const count = Math.ceil(data.length / size); + return [...new Array(count)].map((_, i) => + data.slice(i * size, (i + 1) * size), + ); +} + +/** + * Symbol for timeout. + */ +export const Timeout = Symbol("timeout"); + +/** + * Limit a promise run to a given timeout. + * + * If the promise doesn't resolve in time, the `Timeout` symbol will be returned. + * Otherwise, the promise's result will be passed through. + */ +export function withTimeout( + promise: Promise, + timeoutSeconds: number, +): Promise { + return Promise.race([sleep(timeoutSeconds, Timeout), promise]); +} + +/** + * Generate an array of numbers 0..n + */ +export function range(n: number): number[] { + return [...new Array(Math.max(~~n, 0))].map((_, i) => i); +} + +/** + * Generate all possible combinations of the values in the given arrays. + * + * For example: + * ```js + * console.log(['a', 'b'], [0, 1], [true, false]) + * // ['a', 0, true] + * // ['a', 0, false] + * // ['a', 1, true] + * // ['a', 1, false] + * // ['b', 0, true] + * // ['b', 0, false] + * // ['b', 1, true] + * // ['b', 1, false] + * ``` + * @param {...Array} arrays Arrays to combine + * @returns {Array>} Array of combinations + * @template T + */ +export function combine(...arrays: Array): Array> { + const count = arrays.map((a) => a.length).reduce((a, b) => a * b, 1); + const dimensions = arrays.length; + + const result = range(count) as any[]; + + for (let i = 0; i < count; ++i) { + const item = new Array(dimensions); + let idx = i; + + for (let d = 0; d < dimensions; ++d) { + const divisor = arrays[d].length; + item[d] = arrays[d][idx % divisor]; + idx = ~~(idx / divisor); + } + + result[i] = item; + } + + return result; +} + +/** + * Format a size in bytes to a human readable form. + * + * For example, 131072 becomes 128kb. + */ +export function formatByteSize(size: number): string { + const postfixes = ["b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"]; + const pfi = + (postfixes.length + + postfixes.findIndex((_, i) => size < Math.pow(1024, i + 1))) % + postfixes.length; + + return size / Math.pow(1024, pfi) + postfixes[pfi]; +} + +/** + * Format a duration to a human readable form. + * + * For example, 720 becomes 12min. + */ +export function formatDuration(seconds: number): string { + const units = Object.entries({ + us: 0.000001, + ms: 0.001, + sec: 1, + min: 60, + hr: 3600, + day: 86400, + wk: 604800, + mo: 2592000, + yr: 31536000, + }).reverse(); + + const [unit, multiplier] = + units.find(([_, f]) => seconds > f) ?? units.at(-1)!!; + + return seconds / multiplier + unit; +} + +/** + * Select random words from wordlist. + * + * Used to generate a word based OID For example, FalconTimberYolk + */ +export function generateWordId(wordCount: number = 3): string { + return range(wordCount) + .map(() => words[randomInt(words.length)]) + .join(""); +} diff --git a/src/wordlist.mjs b/src/wordlist.mjs deleted file mode 100644 index 588c904..0000000 --- a/src/wordlist.mjs +++ /dev/null @@ -1,2640 +0,0 @@ -export default [ - 'Abacus', - 'Accordion', - 'Acorn', - 'Acrobat', - 'Adult', - 'Aeroplane', - 'Air', - 'Airplane', - 'Alarm', - 'Albatross', - 'Algae', - 'Almond', - 'Alphabet', - 'Ambulance', - 'America', - 'Anatomy', - 'Anchor', - 'Andromeda', - 'Angel', - 'Ant', - 'Apartment', - 'Apology', - 'Apparatus', - 'Apparel', - 'Appeal', - 'Appendix', - 'Apple', - 'Apricot', - 'Aquarium', - 'Arc', - 'Archipelago', - 'Architecture', - 'Argument', - 'Armadillo', - 'Armchair', - 'Armor', - 'Army', - 'Arrow', - 'Artichoke', - 'Artist', - 'Astronaut', - 'Asylum', - 'Athlete', - 'Atmosphere', - 'Atom', - 'Audience', - 'Aurora', - 'Author', - 'Automobile', - 'Autumn', - 'Avalanche', - 'Avenue', - 'Avocado', - 'Award', - 'Awkwardness', - 'Axe', - 'Baboon', - 'Baby', - 'Backbone', - 'Backpack', - 'Bacon', - 'Badger', - 'Bag', - 'Baggage', - 'Bagpipe', - 'Bail', - 'Bait', - 'Baker', - 'Balance', - 'Balcony', - 'Ball', - 'Balloon', - 'Ballot', - 'Bamboo', - 'Banana', - 'Bandana', - 'Bandit', - 'Bangle', - 'Banyan', - 'Bar', - 'Barbecue', - 'Barber', - 'Bargain', - 'Bark', - 'Barley', - 'Barn', - 'Barrel', - 'Basement', - 'Basket', - 'Basketball', - 'Bassoon', - 'Bat', - 'Bath', - 'Battery', - 'Battle', - 'Bay', - 'Beach', - 'Bead', - 'Beak', - 'Beam', - 'Bean', - 'Bear', - 'Beard', - 'Beast', - 'Beat', - 'Beauty', - 'Beaver', - 'Bed', - 'Bedroom', - 'Bee', - 'Beech', - 'Beef', - 'Beer', - 'Beetle', - 'Beggar', - 'Beginner', - 'Beginning', - 'Behavior', - 'Bell', - 'Bellows', - 'Belt', - 'Bench', - 'Bicycle', - 'Bigfoot', - 'Bike', - 'Bill', - 'Bird', - 'Birth', - 'Biscuit', - 'Bit', - 'Blackbird', - 'Blackboard', - 'Blackcurrant', - 'Bladder', - 'Blade', - 'Blanket', - 'Blazer', - 'Blizzard', - 'Block', - 'Blood', - 'Blouse', - 'Blueberry', - 'Boar', - 'Board', - 'Boat', - 'Body', - 'Bologna', - 'Bolt', - 'Bomb', - 'Bone', - 'Bonfire', - 'Bonnet', - 'Book', - 'Bookcase', - 'Boomerang', - 'Boot', - 'Border', - 'Bottle', - 'Bottom', - 'Boulder', - 'Bouquet', - 'Bow', - 'Bowl', - 'Bowler', - 'Box', - 'Boy', - 'Brain', - 'Brake', - 'Branch', - 'Brass', - 'Bread', - 'Breast', - 'Breath', - 'Breeze', - 'Brick', - 'Bridge', - 'Briefcase', - 'Brilliance', - 'Broccoli', - 'Bronze', - 'Brother', - 'Brownie', - 'Brush', - 'Bubble', - 'Bucket', - 'Buffalo', - 'Bug', - 'Building', - 'Bulb', - 'Bull', - 'Bumblebee', - 'Bunker', - 'Bureau', - 'Burger', - 'Burglary', - 'Bus', - 'Bush', - 'Business', - 'Bust', - 'Butter', - 'Butterfly', - 'Button', - 'Buzzard', - 'Cabbage', - 'Cabin', - 'Cabinet', - 'Cable', - 'Cactus', - 'Cadet', - 'Cafe', - 'Cage', - 'Cake', - 'Calculator', - 'Calf', - 'Calm', - 'Camel', - 'Camera', - 'Camp', - 'Can', - 'Canal', - 'Candle', - 'Candy', - 'Cane', - 'Cannibal', - 'Cannon', - 'Canoe', - 'Canyon', - 'Cap', - 'Cape', - 'Capital', - 'Capitalism', - 'Capitol', - 'Captain', - 'Car', - 'Caravan', - 'Carbon', - 'Card', - 'Cardboard', - 'Care', - 'Career', - 'Cargo', - 'Carpet', - 'Carrot', - 'Cart', - 'Carton', - 'Cartoon', - 'Case', - 'Cash', - 'Castle', - 'Cat', - 'Caterpillar', - 'Cathedral', - 'Cattle', - 'Cave', - 'Cavern', - 'CD', - 'Cedar', - 'Cell', - 'Cellar', - 'Cello', - 'Cellophane', - 'Cement', - 'Cemetery', - 'Cereal', - 'Chain', - 'Chair', - 'Chalk', - 'Challenge', - 'Chamber', - 'Champagne', - 'Champion', - 'Chance', - 'Channel', - 'Chaos', - 'Chapel', - 'Chapter', - 'Character', - 'Charity', - 'Charm', - 'Chart', - 'Chase', - 'Chateau', - 'Cheese', - 'Cheetah', - 'Chef', - 'Chemistry', - 'Cherry', - 'Chess', - 'Chest', - 'Chicken', - 'Child', - 'Chimpanzee', - 'Chin', - 'Chip', - 'Chocolate', - 'Choice', - 'Choir', - 'Chowder', - 'Christmas', - 'Chrome', - 'Church', - 'Cider', - 'Cigar', - 'Cigarette', - 'Cinema', - 'Circle', - 'Circus', - 'City', - 'Clam', - 'Clarity', - 'Class', - 'Classic', - 'Classroom', - 'Claw', - 'Clay', - 'Cleaner', - 'Clearing', - 'Cleavage', - 'Clerk', - 'Cliff', - 'Climate', - 'Climax', - 'Climbing', - 'Clinic', - 'Clip', - 'Clock', - 'Closet', - 'Cloth', - 'Clothes', - 'Cloud', - 'Clover', - 'Club', - 'Coach', - 'Coal', - 'Coast', - 'Coat', - 'Cobra', - 'Cobweb', - 'Cocktail', - 'Cocoa', - 'Coffee', - 'Coffin', - 'Coin', - 'Cola', - 'Cold', - 'Collar', - 'Collection', - 'College', - 'Colony', - 'Color', - 'Column', - 'Comb', - 'Comet', - 'Comfort', - 'Comic', - 'Command', - 'Comment', - 'Commerce', - 'Committee', - 'Common', - 'Communication', - 'Community', - 'Company', - 'Compass', - 'Competition', - 'Composer', - 'Compromise', - 'Computer', - 'Concentration', - 'Concept', - 'Concern', - 'Concert', - 'Conclusion', - 'Concrete', - 'Condition', - 'Conduct', - 'Cone', - 'Conference', - 'Confession', - 'Confidence', - 'Conflict', - 'Confusion', - 'Congo', - 'Congress', - 'Conifer', - 'Connection', - 'Consequence', - 'Conservatory', - 'Consideration', - 'Consolation', - 'Conspiracy', - 'Constant', - 'Construction', - 'Consul', - 'Container', - 'Content', - 'Contention', - 'Continent', - 'Contract', - 'Control', - 'Cook', - 'Cooperation', - 'Copper', - 'Copy', - 'Coral', - 'Cord', - 'Cork', - 'Corn', - 'Corner', - 'Coronation', - 'Corporation', - 'Corridor', - 'Corruption', - 'Cosmos', - 'Cost', - 'Costume', - 'Cottage', - 'Cotton', - 'Cough', - 'Council', - 'Count', - 'Counter', - 'Country', - 'Courage', - 'Course', - 'Court', - 'Cousin', - 'Covenant', - 'Cover', - 'Cow', - 'Cowboy', - 'Crab', - 'Cracker', - 'Cradle', - 'Craft', - 'Crane', - 'Crate', - 'Crater', - 'Crayon', - 'Cream', - 'Creativity', - 'Creature', - 'Creek', - 'Crepe', - 'Crib', - 'Cricket', - 'Crime', - 'Crisis', - 'Criterion', - 'Criticism', - 'Crocodile', - 'Crop', - 'Cross', - 'Crow', - 'Crown', - 'Cruiser', - 'Crumb', - 'Crush', - 'Cry', - 'Crystal', - 'Cube', - 'Cuckoo', - 'Cucumber', - 'Cult', - 'Culture', - 'Cup', - 'Cupboard', - 'Curiosity', - 'Curtain', - 'Cushion', - 'Custom', - 'Cut', - 'Cutter', - 'Cycle', - 'Cylinder', - 'Cymbal', - 'Cypress', - 'Daddy', - 'Daffodil', - 'Dagger', - 'Dahlia', - 'Dairy', - 'Dam', - 'Damage', - 'Dance', - 'Danger', - 'Dare', - 'Darkness', - 'Dart', - 'Dash', - 'Database', - 'Date', - 'Daughter', - 'Dawn', - 'Day', - 'Daylight', - 'Deacon', - 'Deal', - 'Dean', - 'Death', - 'Debate', - 'Debt', - 'Decade', - 'Decay', - 'Deception', - 'Decision', - 'Deck', - 'Decline', - 'Decoration', - 'Decree', - 'Deer', - 'Defence', - 'Deficit', - 'Definition', - 'Degree', - 'Delay', - 'Delegation', - 'Delight', - 'Delivery', - 'Demand', - 'Democracy', - 'Demon', - 'Den', - 'Dentist', - 'Department', - 'Departure', - 'Deposit', - 'Depression', - 'Depth', - 'Deputy', - 'Desert', - 'Desire', - 'Desk', - 'Despair', - 'Dessert', - 'Destination', - 'Detail', - 'Detective', - 'Determinant', - 'Development', - 'Device', - 'Devotion', - 'Dew', - 'Diagram', - 'Dialect', - 'Diamond', - 'Diaphragm', - 'Diary', - 'Dictator', - 'Dictionary', - 'Dinosaur', - 'Diploma', - 'Direction', - 'Director', - 'Dirt', - 'Disability', - 'Disadvantage', - 'Disaster', - 'Disc', - 'Discipline', - 'Disclosure', - 'Discount', - 'Discovery', - 'Discussion', - 'Disease', - 'Dish', - 'Disk', - 'Disposal', - 'Distance', - 'Distinction', - 'Disturbance', - 'Dive', - 'Diversity', - 'Division', - 'Doctor', - 'Doctrine', - 'Document', - 'Dog', - 'Dollar', - 'Doll', - 'Dolphin', - 'Donation', - 'Donkey', - 'Door', - 'Doorknob', - 'Dot', - 'Doubt', - 'Dough', - 'Dove', - 'Downpour', - 'Dragon', - 'Drain', - 'Drama', - 'Drawer', - 'Dream', - 'Dressing', - 'Drill', - 'Drink', - 'Drive', - 'Driver', - 'Driving', - 'Drizzle', - 'Drone', - 'Drop', - 'Drum', - 'Duck', - 'Dune', - 'Dungeon', - 'Durian', - 'Dust', - 'Duty', - 'Dwarf', - 'Dye', - 'Eagle', - 'Ear', - 'Earth', - 'Earthquake', - 'Ease', - 'East', - 'Echo', - 'Economy', - 'Edge', - 'Edifice', - 'Editor', - 'Education', - 'Effect', - 'Effort', - 'Egg', - 'Eggplant', - 'Ego', - 'Elbow', - 'Election', - 'Electricity', - 'Elephant', - 'Elevator', - 'Elite', - 'Elk', - 'Embarrassment', - 'Embrace', - 'Emerald', - 'Emotion', - 'Emperor', - 'Emphasis', - 'Empire', - 'Employee', - 'Employer', - 'Employment', - 'End', - 'Endurance', - 'Energy', - 'Engine', - 'Engineer', - 'Enigma', - 'Enjoyment', - 'Enough', - 'Enquiry', - 'Enterprise', - 'Entertainment', - 'Enthusiasm', - 'Entrance', - 'Environment', - 'Episode', - 'Equality', - 'Equation', - 'Equinox', - 'Equipment', - 'Era', - 'Eruption', - 'Escalator', - 'Escape', - 'Espresso', - 'Essence', - 'Estate', - 'Estimate', - 'Eternity', - 'Ethics', - 'Europe', - 'Evaluation', - 'Eve', - 'Evening', - 'Event', - 'Evidence', - 'Evil', - 'Evolution', - 'Example', - 'Excavation', - 'Excellence', - 'Exception', - 'Exchange', - 'Excitement', - 'Excuse', - 'Execution', - 'Executive', - 'Exercise', - 'Exhibition', - 'Existence', - 'Exit', - 'Expansion', - 'Expectation', - 'Expedition', - 'Expense', - 'Experience', - 'Experiment', - 'Expert', - 'Explanation', - 'Exploration', - 'Explosion', - 'Exposure', - 'Extension', - 'Extent', - 'External', - 'Eye', - 'Eyeball', - 'Eyebrow', - 'Eyelash', - 'Eyelid', - 'Fabric', - 'Face', - 'Facility', - 'Fact', - 'Factor', - 'Faculty', - 'Fading', - 'Failure', - 'Fair', - 'Faith', - 'Falcon', - 'Fall', - 'Family', - 'Fan', - 'Fantasy', - 'Farm', - 'Farmer', - 'Fascination', - 'Fashion', - 'Fate', - 'Father', - 'Fatigue', - 'Fault', - 'Fear', - 'Feast', - 'Feather', - 'Feature', - 'Federation', - 'Fee', - 'Feedback', - 'Feeling', - 'Feet', - 'Fence', - 'Ferret', - 'Festival', - 'Fiber', - 'Fiction', - 'Fiddle', - 'Field', - 'Fig', - 'Fight', - 'Figure', - 'File', - 'Film', - 'Filter', - 'Fin', - 'Finance', - 'Finding', - 'Finger', - 'Finish', - 'Fire', - 'Firefighter', - 'Firefly', - 'Firm', - 'Fish', - 'Fisherman', - 'Fitness', - 'Fix', - 'Fixture', - 'Flag', - 'Flame', - 'Flamingo', - 'Flash', - 'Flax', - 'Flea', - 'Fleet', - 'Flesh', - 'Flight', - 'Flock', - 'Flood', - 'Floor', - 'Flour', - 'Flower', - 'Fluid', - 'Flute', - 'Fly', - 'Foam', - 'Fog', - 'Foil', - 'Folder', - 'Folk', - 'Food', - 'Foot', - 'Football', - 'Force', - 'Forecast', - 'Forehead', - 'Forest', - 'Fork', - 'Form', - 'Format', - 'Former', - 'Fort', - 'Fortune', - 'Foundation', - 'Fountain', - 'Fox', - 'Fraction', - 'Fragment', - 'Frame', - 'France', - 'Freedom', - 'Freeze', - 'Freezer', - 'Freight', - 'French', - 'Frequency', - 'Friction', - 'Friend', - 'Frog', - 'Front', - 'Frost', - 'Frown', - 'Fruit', - 'Fuel', - 'Function', - 'Funding', - 'Funeral', - 'Fur', - 'Furnace', - 'Furniture', - 'Future', - 'Gadget', - 'Gaffer', - 'Gain', - 'Gait', - 'Galaxy', - 'Gallery', - 'Galleon', - 'Gallon', - 'Game', - 'Garden', - 'Garlic', - 'Garment', - 'Gas', - 'Gate', - 'Gathering', - 'Gauge', - 'Gazelle', - 'Gear', - 'Geese', - 'Gem', - 'Gender', - 'Gene', - 'General', - 'Genius', - 'Genre', - 'Gentleman', - 'Geography', - 'Geology', - 'Geometry', - 'Geranium', - 'Germany', - 'Gesture', - 'Ghost', - 'Giant', - 'Gift', - 'Giraffe', - 'Girl', - 'Glacier', - 'Gladness', - 'Glass', - 'Glaze', - 'Glimpse', - 'Globe', - 'Gloom', - 'Glory', - 'Glove', - 'Glow', - 'Glue', - 'Goal', - 'Goat', - 'God', - 'Gold', - 'Goldfish', - 'Golf', - 'Gondola', - 'Gong', - 'Good', - 'Goodness', - 'Goose', - 'Gorilla', - 'Gossip', - 'Government', - 'Governor', - 'Gown', - 'Grace', - 'Grade', - 'Grain', - 'Grammar', - 'Grandchild', - 'Grandfather', - 'Grandmother', - 'Grandparent', - 'Grandson', - 'Grape', - 'Graph', - 'Grass', - 'Grasshopper', - 'Gratitude', - 'Grave', - 'Gravity', - 'Grease', - 'Greatness', - 'Greenhouse', - 'Greeting', - 'Grid', - 'Grief', - 'Grille', - 'Grimace', - 'Grind', - 'Grit', - 'Grocery', - 'Ground', - 'Group', - 'Grouse', - 'Growth', - 'Guarantee', - 'Guard', - 'Guess', - 'Guest', - 'Guidance', - 'Guide', - 'Guitar', - 'Gum', - 'Gun', - 'Gutter', - 'Gym', - 'Habitat', - 'Hair', - 'Haircut', - 'Hall', - 'Hamburger', - 'Hammer', - 'Hamster', - 'Hand', - 'Handbag', - 'Handicap', - 'Handle', - 'Handsaw', - 'Happiness', - 'Harbor', - 'Hardship', - 'Hare', - 'Harm', - 'Harmony', - 'Harp', - 'Harvest', - 'Hat', - 'Hate', - 'Hawk', - 'Hazard', - 'Head', - 'Headache', - 'Headland', - 'Headlight', - 'Headline', - 'Headquarters', - 'Health', - 'Hearing', - 'Heart', - 'Heat', - 'Heather', - 'Heaven', - 'Hedge', - 'Hedgehog', - 'Height', - 'Helicopter', - 'Hell', - 'Helmet', - 'Help', - 'Hemisphere', - 'Herb', - 'Heron', - 'Hero', - 'Herring', - 'Hesitation', - 'Hide', - 'Highway', - 'Hill', - 'Hip', - 'Hippopotamus', - 'Hire', - 'Historian', - 'History', - 'Hobbit', - 'Hockey', - 'Hold', - 'Hole', - 'Holiday', - 'Holland', - 'Home', - 'Honey', - 'Honeydew', - 'Honesty', - 'Honor', - 'Hood', - 'Hoof', - 'Hook', - 'Hope', - 'Horizon', - 'Horn', - 'Horse', - 'Hospital', - 'Host', - 'Hostel', - 'Hotdog', - 'Hotel', - 'Hour', - 'House', - 'Hovercraft', - 'Hummingbird', - 'Humor', - 'Hunter', - 'Hurricane', - 'Hurry', - 'Husband', - 'Hut', - 'Hyacinth', - 'Hydrant', - 'Hyena', - 'Hygiene', - 'Hypothesis', - 'Ice', - 'Iceberg', - 'Icicle', - 'Idea', - 'Ideal', - 'Identity', - 'Ideology', - 'Idiot', - 'Igloo', - 'Ignorance', - 'Illusion', - 'Image', - 'Imagination', - 'Impact', - 'Implement', - 'Importance', - 'Impression', - 'Improvement', - 'Incense', - 'Incentive', - 'Inch', - 'Incidence', - 'Income', - 'Increase', - 'Independence', - 'Index', - 'India', - 'Indication', - 'Individual', - 'Industry', - 'Infant', - 'Infection', - 'Influence', - 'Information', - 'Ingenuity', - 'Ingredient', - 'Inhabitant', - 'Inheritance', - 'Initial', - 'Initiative', - 'Injection', - 'Injury', - 'Ink', - 'Inquiry', - 'Insect', - 'Inside', - 'Insight', - 'Insistence', - 'Inspection', - 'Inspector', - 'Inspiration', - 'Installment', - 'Instance', - 'Instinct', - 'Institute', - 'Institution', - 'Instruction', - 'Instrument', - 'Insulation', - 'Insurance', - 'Intelligence', - 'Intention', - 'Interest', - 'Interference', - 'Interior', - 'Internet', - 'Interpretation', - 'Interruption', - 'Interview', - 'Introduction', - 'Intuition', - 'Invention', - 'Inventor', - 'Investment', - 'Invitation', - 'Invoice', - 'Iron', - 'Island', - 'Issue', - 'Italy', - 'Item', - 'Jackal', - 'Jacket', - 'Jaguar', - 'Jail', - 'Jalapeño', - 'Jam', - 'Jar', - 'Jasmine', - 'Javelin', - 'Jaw', - 'Jeans', - 'Jelly', - 'Jellyfish', - 'Jet', - 'Jewel', - 'Jewelry', - 'Job', - 'Jockey', - 'Joker', - 'Journey', - 'Joy', - 'Judge', - 'Judgment', - 'Juice', - 'Jumper', - 'Junction', - 'Jungle', - 'Juniper', - 'Junk', - 'Justice', - 'Kangaroo', - 'Karate', - 'Kayaking', - 'Ketchup', - 'Kettle', - 'Key', - 'Keyboard', - 'Kick', - 'Kid', - 'Kidney', - 'Kilometer', - 'King', - 'Kingdom', - 'Kiss', - 'Kitchen', - 'Kite', - 'Kitten', - 'Knapsack', - 'Knee', - 'Kneel', - 'Knife', - 'Knight', - 'Knitting', - 'Knob', - 'Knock', - 'Knot', - 'Knowledge', - 'Koala', - 'Lab', - 'Label', - 'Labor', - 'Laboratory', - 'Lace', - 'Lack', - 'Ladder', - 'Lad', - 'Lady', - 'Lake', - 'Lamb', - 'Lamp', - 'Lancet', - 'Land', - 'Landscape', - 'Lane', - 'Language', - 'Lantern', - 'Lap', - 'Lapel', - 'Larch', - 'Lard', - 'Lark', - 'Laughter', - 'Launch', - 'Lavender', - 'Law', - 'Lawn', - 'Lawyer', - 'Lead', - 'Leader', - 'Leaf', - 'League', - 'Leak', - 'Leather', - 'Lecture', - 'Leg', - 'Legacy', - 'Legal', - 'Legend', - 'Legislation', - 'Leisure', - 'Lemon', - 'Length', - 'Lentil', - 'Leopard', - 'Letter', - 'Lettuce', - 'Level', - 'Lever', - 'Liability', - 'Library', - 'License', - 'Lid', - 'Life', - 'Lift', - 'Light', - 'Lightning', - 'Lilac', - 'Lily', - 'Limb', - 'Lime', - 'Limit', - 'Line', - 'Linen', - 'Link', - 'Lion', - 'Lip', - 'Lipstick', - 'Liquid', - 'List', - 'Listen', - 'Literacy', - 'Literature', - 'Litter', - 'Liver', - 'Lizard', - 'Llama', - 'Loaf', - 'Lobby', - 'Lobster', - 'Local', - 'Location', - 'Lock', - 'Locker', - 'Locust', - 'Loft', - 'Log', - 'Logic', - 'Lollipop', - 'Loneliness', - 'Longboat', - 'Look', - 'Loop', - 'Lord', - 'Loss', - 'Lot', - 'Lotion', - 'Lounge', - 'Love', - 'Lover', - 'Loyalty', - 'Luck', - 'Luggage', - 'Lullaby', - 'Lunch', - 'Lungs', - 'Lute', - 'Luxury', - 'Lynx', - 'Lyre', - 'Macaroon', - 'Machine', - 'Madness', - 'Magazine', - 'Magic', - 'Magnet', - 'Magnitude', - 'Maiden', - 'Mail', - 'Mailbox', - 'Mainland', - 'Maintenance', - 'Majority', - 'Make', - 'Maker', - 'Malaria', - 'Male', - 'Mall', - 'Man', - 'Management', - 'Manager', - 'Mango', - 'Mansion', - 'Manual', - 'Manufacture', - 'Map', - 'Maple', - 'Marble', - 'March', - 'Mare', - 'Margin', - 'Mark', - 'Market', - 'Marketing', - 'Marmalade', - 'Marriage', - 'Marsh', - 'Marshmallow', - 'Mask', - 'Mass', - 'Master', - 'Match', - 'Material', - 'Math', - 'Matter', - 'Mattress', - 'Maximum', - 'Mayonnaise', - 'Maze', - 'Meadow', - 'Meal', - 'Meaning', - 'Measure', - 'Meat', - 'Mechanic', - 'Mechanism', - 'Medal', - 'Medicine', - 'Mediocrity', - 'Meeting', - 'Melody', - 'Melon', - 'Member', - 'Memo', - 'Memory', - 'Men', - 'Mention', - 'Menu', - 'Mercy', - 'Mess', - 'Message', - 'Metal', - 'Meter', - 'Method', - 'Mexico', - 'Microphone', - 'Microscope', - 'Midday', - 'Middle', - 'Midnight', - 'Might', - 'Migration', - 'Milk', - 'Mill', - 'Millennium', - 'Mind', - 'Mine', - 'Mineral', - 'Minibus', - 'Minimum', - 'Minister', - 'Minority', - 'Mint', - 'Minute', - 'Mirror', - 'Missile', - 'Mission', - 'Mist', - 'Mistake', - 'Mister', - 'Mitten', - 'Mix', - 'Mixture', - 'Mobile', - 'Mode', - 'Model', - 'Modesty', - 'Mole', - 'Moment', - 'Monarch', - 'Money', - 'Monitor', - 'Monkey', - 'Monocle', - 'Monster', - 'Month', - 'Monument', - 'Mood', - 'Moon', - 'Moose', - 'Moral', - 'Morning', - 'Moth', - 'Mother', - 'Motion', - 'Motivation', - 'Motor', - 'Motorcycle', - 'Mound', - 'Mountain', - 'Mouse', - 'Mouth', - 'Movement', - 'Movie', - 'Muffin', - 'Mule', - 'Multiple', - 'Mummy', - 'Muscle', - 'Museum', - 'Mushroom', - 'Music', - 'Musician', - 'Mustard', - 'Mystery', - 'Myth', - 'Nail', - 'Name', - 'Napkin', - 'Nation', - 'Nature', - 'Navel', - 'Navigation', - 'Nebula', - 'Neck', - 'Necklace', - 'Need', - 'Needle', - 'Negative', - 'Negotiation', - 'Neighbor', - 'Neighborhood', - 'Nerve', - 'Nest', - 'Net', - 'Network', - 'News', - 'Newspaper', - 'Niche', - 'Night', - 'Nightmare', - 'Nightingale', - 'Noise', - 'Noodle', - 'North', - 'Nose', - 'Note', - 'Notebook', - 'Nothing', - 'Notice', - 'Notion', - 'Novel', - 'Novelty', - 'Nuisance', - 'Number', - 'Nurse', - 'Nut', - 'Nutrition', - 'Nylon', - 'Oak', - 'Oar', - 'Oasis', - 'Objection', - 'Objective', - 'Obligation', - 'Observation', - 'Observer', - 'Obstacle', - 'Occasion', - 'Occupation', - 'Ocean', - 'Ocelot', - 'Octopus', - 'Offence', - 'Offer', - 'Office', - 'Officer', - 'Official', - 'Oil', - 'Ointment', - 'Old', - 'Olive', - 'Omnibus', - 'Onion', - 'Open', - 'Opening', - 'Opera', - 'Operation', - 'Opinion', - 'Opportunity', - 'Opponent', - 'Optics', - 'Option', - 'Orange', - 'Orchestra', - 'Order', - 'Organ', - 'Organization', - 'Orientation', - 'Origin', - 'Ornament', - 'Ostrich', - 'Otter', - 'Outcome', - 'Outfit', - 'Outlet', - 'Outline', - 'Output', - 'Oval', - 'Oven', - 'Overalls', - 'Overcoat', - 'Overhang', - 'Overload', - 'Owl', - 'Owner', - 'Oxygen', - 'Oyster', - 'Pace', - 'Package', - 'Packet', - 'Pad', - 'Page', - 'Pain', - 'Paint', - 'Painting', - 'Pair', - 'Palace', - 'Paleontologist', - 'Palm', - 'Pan', - 'Panda', - 'Panel', - 'Panic', - 'Pants', - 'Paper', - 'Paperback', - 'Parade', - 'Paradise', - 'Paragraph', - 'Parallel', - 'Parasite', - 'Parent', - 'Park', - 'Parrot', - 'Part', - 'Partner', - 'Party', - 'Passage', - 'Past', - 'Pastor', - 'Patch', - 'Path', - 'Patience', - 'Patient', - 'Patio', - 'Patrol', - 'Pattern', - 'Pause', - 'Pavement', - 'Paw', - 'Payment', - 'Pea', - 'Peace', - 'Peach', - 'Peak', - 'Pear', - 'Pearl', - 'Peasant', - 'Pebble', - 'Pedal', - 'Pedestrian', - 'Peel', - 'Pen', - 'Penalty', - 'Pencil', - 'Penguin', - 'Pension', - 'People', - 'Pepper', - 'Percentage', - 'Perception', - 'Performance', - 'Period', - 'Permission', - 'Permit', - 'Persistence', - 'Person', - 'Personality', - 'Perspective', - 'Pet', - 'Petition', - 'Phase', - 'Pheasant', - 'Phenomenon', - 'Philosophy', - 'Phone', - 'Photo', - 'Photography', - 'Phrase', - 'Physics', - 'Piano', - 'Pickle', - 'Picture', - 'Pie', - 'Piece', - 'Pig', - 'Pigeon', - 'Pigment', - 'Pile', - 'Pill', - 'Pillar', - 'Pillow', - 'Pilot', - 'Pine', - 'Pineapple', - 'Pink', - 'Pint', - 'Pipe', - 'Pistol', - 'Pit', - 'Pitch', - 'Pizza', - 'Place', - 'Plain', - 'Plan', - 'Plane', - 'Planet', - 'Plant', - 'Plastic', - 'Plate', - 'Platform', - 'Play', - 'Player', - 'Playground', - 'Pleasure', - 'Pledge', - 'Plot', - 'Plug', - 'Plum', - 'Plumber', - 'Pocket', - 'Poem', - 'Poet', - 'Point', - 'Poison', - 'Pole', - 'Police', - 'Policy', - 'Politeness', - 'Poll', - 'Pollen', - 'Polo', - 'Pond', - 'Pony', - 'Pool', - 'Popcorn', - 'Pope', - 'Poplar', - 'Poppy', - 'Popularity', - 'Population', - 'Porcelain', - 'Porch', - 'Porcupine', - 'Port', - 'Porter', - 'Portfolio', - 'Portion', - 'Portrait', - 'Position', - 'Possession', - 'Possibility', - 'Post', - 'Postage', - 'Poster', - 'Pot', - 'Potato', - 'Potential', - 'Pound', - 'Poverty', - 'Powder', - 'Power', - 'Practice', - 'Praise', - 'Pray', - 'Prayer', - 'Preacher', - 'Precedence', - 'Precision', - 'Prediction', - 'Preference', - 'Preparation', - 'Presence', - 'Present', - 'Presentation', - 'Preservation', - 'President', - 'Press', - 'Pressure', - 'Prestige', - 'Prevention', - 'Price', - 'Pride', - 'Priest', - 'Primary', - 'Prime', - 'Prince', - 'Princess', - 'Principal', - 'Principle', - 'Print', - 'Prior', - 'Priority', - 'Prison', - 'Prisoner', - 'Privacy', - 'Prize', - 'Problem', - 'Procedure', - 'Process', - 'Prodigy', - 'Produce', - 'Product', - 'Profession', - 'Professor', - 'Profit', - 'Program', - 'Progress', - 'Project', - 'Promise', - 'Promotion', - 'Proof', - 'Property', - 'Proposal', - 'Prosecution', - 'Prospect', - 'Protection', - 'Protest', - 'Proud', - 'Province', - 'Provision', - 'Psychology', - 'Pub', - 'Public', - 'Publication', - 'Publicity', - 'Publisher', - 'Pudding', - 'Puddle', - 'Puffin', - 'Pull', - 'Pulpit', - 'Pulse', - 'Puma', - 'Pump', - 'Pumpkin', - 'Punch', - 'Punctuation', - 'Punishment', - 'Puppet', - 'Puppy', - 'Purchase', - 'Purpose', - 'Purse', - 'Pursuit', - 'Push', - 'Put', - 'Pyramid', - 'Quadrant', - 'Quail', - 'Quality', - 'Quantity', - 'Quartz', - 'Queen', - 'Quill', - 'Quilt', - 'Quince', - 'Quit', - 'Rabbit', - 'Raccoon', - 'Race', - 'Rack', - 'Radar', - 'Radiator', - 'Radio', - 'Radish', - 'Raft', - 'Rag', - 'Rail', - 'Rain', - 'Rainbow', - 'Raincoat', - 'Ram', - 'Range', - 'Rank', - 'Rapids', - 'Rate', - 'Raven', - 'Raw', - 'Ray', - 'Razor', - 'Reaction', - 'Reader', - 'Reading', - 'Reality', - 'Reason', - 'Reception', - 'Recipe', - 'Recognition', - 'Recommendation', - 'Record', - 'Recording', - 'Recovery', - 'Recreation', - 'Rectangle', - 'Redcurrant', - 'Redoubt', - 'Reed', - 'Reference', - 'Refinement', - 'Reflection', - 'Reform', - 'Refusal', - 'Regard', - 'Regret', - 'Regular', - 'Regulation', - 'Rein', - 'Reindeer', - 'Relation', - 'Relationship', - 'Relaxation', - 'Release', - 'Relief', - 'Religion', - 'Remark', - 'Remedy', - 'Reminder', - 'Removal', - 'Rent', - 'Repair', - 'Repeat', - 'Replacement', - 'Reply', - 'Report', - 'Reporter', - 'Representation', - 'Representative', - 'Reproduction', - 'Reputation', - 'Request', - 'Requirement', - 'Research', - 'Reserve', - 'Resident', - 'Resistance', - 'Resolution', - 'Resort', - 'Resource', - 'Respect', - 'Response', - 'Responsibility', - 'Rest', - 'Restaurant', - 'Result', - 'Retail', - 'Retirement', - 'Return', - 'Revelation', - 'Revenge', - 'Revenue', - 'Reverse', - 'Review', - 'Revolution', - 'Reward', - 'Rhetoric', - 'Rhinoceros', - 'Rhythm', - 'Rib', - 'Ribbon', - 'Rice', - 'Riddle', - 'Ridge', - 'Rifle', - 'Right', - 'Ring', - 'Rip', - 'Rise', - 'Risk', - 'Ritual', - 'River', - 'Road', - 'Roadrunner', - 'Roast', - 'Robbery', - 'Robot', - 'Rock', - 'Rocket', - 'Rodent', - 'Role', - 'Roll', - 'Romance', - 'Roof', - 'Room', - 'Rooster', - 'Root', - 'Rope', - 'Rose', - 'Rotation', - 'Rough', - 'Round', - 'Route', - 'Routine', - 'Row', - 'Royal', - 'Rubbish', - 'Ruby', - 'Rucksack', - 'Ruin', - 'Rule', - 'Ruler', - 'Rumor', - 'Run', - 'Runner', - 'Rush', - 'Rust', - 'Rye', - 'Sack', - 'Sacrifice', - 'Sadness', - 'Saddle', - 'Safari', - 'Safety', - 'Saffron', - 'Sage', - 'Sail', - 'Sailor', - 'Salad', - 'Salary', - 'Sale', - 'Salmon', - 'Salt', - 'Sample', - 'Sand', - 'Sandal', - 'Sandwich', - 'Sapphire', - 'Satellite', - 'Satisfaction', - 'Sauce', - 'Saucepan', - 'Sausage', - 'Save', - 'Saw', - 'Scale', - 'Scandal', - 'Scarecrow', - 'Scarf', - 'Scene', - 'Schedule', - 'Scheme', - 'Scholar', - 'Scholarship', - 'School', - 'Science', - 'Scientist', - 'Scoop', - 'Scope', - 'Score', - 'Scorpion', - 'Scout', - 'Scrabble', - 'Scramble', - 'Scrap', - 'Scratch', - 'Screen', - 'Screw', - 'Script', - 'Sculpture', - 'Sea', - 'Seagull', - 'Seal', - 'Search', - 'Seashore', - 'Season', - 'Seat', - 'Secret', - 'Secretary', - 'Section', - 'Sector', - 'Security', - 'Seed', - 'Seek', - 'Selection', - 'Self', - 'Seller', - 'Sense', - 'Sensitivity', - 'Sentence', - 'Separation', - 'September', - 'Sequence', - 'Series', - 'Serve', - 'Server', - 'Service', - 'Session', - 'Set', - 'Setting', - 'Settle', - 'Settlement', - 'Shade', - 'Shadow', - 'Shake', - 'Shame', - 'Shampoo', - 'Shape', - 'Share', - 'Shark', - 'Sheep', - 'Sheet', - 'Shelf', - 'Shell', - 'Shelter', - 'Shepherd', - 'Shield', - 'Shift', - 'Shine', - 'Ship', - 'Shirt', - 'Shock', - 'Shoe', - 'Shooting', - 'Shop', - 'Shopping', - 'Shore', - 'Short', - 'Shortage', - 'Shot', - 'Shoulder', - 'Shout', - 'Show', - 'Shower', - 'Shrimp', - 'Shrine', - 'Sibling', - 'Sick', - 'Side', - 'Sidewalk', - 'Sight', - 'Sign', - 'Signal', - 'Signature', - 'Significance', - 'Silence', - 'Silk', - 'Silver', - 'Similarity', - 'Simon', - 'Simple', - 'Simplicity', - 'Sin', - 'Singer', - 'Single', - 'Sink', - 'Sister', - 'Site', - 'Situation', - 'Size', - 'Sketch', - 'Ski', - 'Skill', - 'Skin', - 'Skirt', - 'Skull', - 'Sky', - 'Slab', - 'Slang', - 'Slave', - 'Slavery', - 'Sleep', - 'Sleeve', - 'Slice', - 'Slide', - 'Slip', - 'Slope', - 'Slot', - 'Sly', - 'Smell', - 'Smile', - 'Smoke', - 'Snail', - 'Snake', - 'Snow', - 'Soap', - 'Society', - 'Sock', - 'Soda', - 'Softness', - 'Soil', - 'Solar', - 'Soldier', - 'Sole', - 'Solicitor', - 'Solid', - 'Solution', - 'Somersault', - 'Son', - 'Song', - 'Soprano', - 'Sore', - 'Sort', - 'Soul', - 'Sound', - 'Soup', - 'Source', - 'South', - 'Sovereignty', - 'Space', - 'Spade', - 'Spaghetti', - 'Spain', - 'Span', - 'Sparrow', - 'Speaker', - 'Special', - 'Specialist', - 'Species', - 'Specification', - 'Speech', - 'Speed', - 'Spell', - 'Spice', - 'Spider', - 'Spike', - 'Spinach', - 'Spirit', - 'Spite', - 'Splash', - 'Spleen', - 'Splendor', - 'Split', - 'Spoon', - 'Sport', - 'Spot', - 'Spring', - 'Squad', - 'Square', - 'Squash', - 'Squirrel', - 'Stability', - 'Stadium', - 'Staff', - 'Stage', - 'Stain', - 'Stair', - 'Stake', - 'Stall', - 'Stamp', - 'Stance', - 'Stand', - 'Standard', - 'Star', - 'Stare', - 'Start', - 'State', - 'Statement', - 'Station', - 'Statistic', - 'Status', - 'Steak', - 'Steam', - 'Steel', - 'Steering', - 'Step', - 'Stick', - 'Sticker', - 'Stomach', - 'Stone', - 'Stool', - 'Stop', - 'Storage', - 'Store', - 'Storm', - 'Story', - 'Stove', - 'Strain', - 'Strait', - 'Strand', - 'Stranger', - 'Strategy', - 'Strawberry', - 'Stream', - 'Street', - 'Strength', - 'Stress', - 'Stretch', - 'Strike', - 'String', - 'Strip', - 'Structure', - 'Struggle', - 'Student', - 'Studio', - 'Study', - 'Stuff', - 'Stupidity', - 'Style', - 'Subject', - 'Submission', - 'Substance', - 'Success', - 'Sugar', - 'Suggestion', - 'Suit', - 'Sulphur', - 'Summit', - 'Sun', - 'Sunshine', - 'Supermarket', - 'Supply', - 'Support', - 'Surgeon', - 'Surgery', - 'Surname', - 'Surprise', - 'Surrender', - 'Surroundings', - 'Survey', - 'Survival', - 'Suspicion', - 'Swallow', - 'Swan', - 'Swear', - 'Sweater', - 'Sweep', - 'Sweet', - 'Sweetness', - 'Swim', - 'Swimming', - 'Swine', - 'Swing', - 'Switch', - 'Sword', - 'Symbol', - 'Sympathy', - 'Syrup', - 'System', - 'Table', - 'Tablet', - 'Taboo', - 'Tackle', - 'Tail', - 'Tailor', - 'Talent', - 'Talk', - 'Tank', - 'Tap', - 'Tape', - 'Target', - 'Task', - 'Taste', - 'Tattoo', - 'Tax', - 'Taxi', - 'Tea', - 'Teacher', - 'Team', - 'Tear', - 'Technique', - 'Technology', - 'Teeth', - 'Telegram', - 'Telephone', - 'Telescope', - 'Television', - 'Temper', - 'Temperature', - 'Temple', - 'Tempo', - 'Temptation', - 'Tendency', - 'Tennis', - 'Tension', - 'Tent', - 'Term', - 'Territory', - 'Terror', - 'Test', - 'Text', - 'Texture', - 'Thank', - 'Thanks', - 'Theme', - 'Theory', - 'Therapy', - 'Thick', - 'Thief', - 'Thing', - 'Think', - 'Thinness', - 'Thought', - 'Thread', - 'Threat', - 'Throat', - 'Throne', - 'Thumb', - 'Thunder', - 'Ticket', - 'Tide', - 'Tiger', - 'Tile', - 'Time', - 'Tin', - 'Tip', - 'Tire', - 'Tissue', - 'Title', - 'Toad', - 'Toast', - 'Tobacco', - 'Today', - 'Toe', - 'Together', - 'Toilet', - 'Toleration', - 'Tomato', - 'Tomorrow', - 'Tongue', - 'Tonight', - 'Tool', - 'Tooth', - 'Toothbrush', - 'Toothpaste', - 'Top', - 'Topic', - 'Torch', - 'Tornado', - 'Tortoise', - 'Torture', - 'Total', - 'Touch', - 'Tough', - 'Tour', - 'Tourism', - 'Tourist', - 'Towel', - 'Tower', - 'Town', - 'Toxic', - 'Toy', - 'Trace', - 'Track', - 'Tractor', - 'Trade', - 'Tradition', - 'Traffic', - 'Tragedy', - 'Trail', - 'Trailer', - 'Train', - 'Trainer', - 'Training', - 'Trait', - 'Transaction', - 'Transcript', - 'Transfer', - 'Transformation', - 'Transition', - 'Translation', - 'Transmission', - 'Transport', - 'Trap', - 'Travel', - 'Tray', - 'Treasure', - 'Treat', - 'Treatment', - 'Tree', - 'Trial', - 'Triangle', - 'Tribe', - 'Trick', - 'Trolley', - 'Trouble', - 'Trousers', - 'Truck', - 'Trumpet', - 'Trunk', - 'Trust', - 'Truth', - 'Tuba', - 'Tube', - 'Tulip', - 'Tuna', - 'Tunnel', - 'Turkey', - 'Turn', - 'Turnip', - 'Turtle', - 'Twin', - 'Twist', - 'Type', - 'Typhoon', - 'Tyre', - 'Uganda', - 'Ukulele', - 'Ultimate', - 'Umbrella', - 'Uncle', - 'Under', - 'Underpants', - 'Understanding', - 'Underwear', - 'Unemployment', - 'Uniform', - 'Union', - 'Unit', - 'United', - 'Unity', - 'Universe', - 'University', - 'Unknown', - 'Unrest', - 'Uranus', - 'Urgent', - 'Urine', - 'Usage', - 'Use', - 'User', - 'Utility', - 'Utopia', - 'Vacancy', - 'Vacation', - 'Vacuum', - 'Valley', - 'Value', - 'Van', - 'Vanilla', - 'Vanish', - 'Variety', - 'Vase', - 'Vastness', - 'Veal', - 'Vector', - 'Vegetable', - 'Vehicle', - 'Veil', - 'Vein', - 'Velvet', - 'Vendor', - 'Ventilation', - 'Venture', - 'Verdict', - 'Verification', - 'Verse', - 'Version', - 'Vessel', - 'Veteran', - 'Vet', - 'Vibration', - 'Vicinity', - 'Victim', - 'Victory', - 'Video', - 'View', - 'Village', - 'Vine', - 'Vinegar', - 'Violation', - 'Violet', - 'Violin', - 'Viper', - 'Virus', - 'Visa', - 'Vision', - 'Visit', - 'Visitor', - 'Visual', - 'Vitality', - 'Vitamin', - 'Vocation', - 'Vodka', - 'Voice', - 'Volcano', - 'Volleyball', - 'Volume', - 'Volunteer', - 'Vomit', - 'Vortex', - 'Vote', - 'Voucher', - 'Vowel', - 'Voyage', - 'Vulture', - 'Waddle', - 'Wafer', - 'Wage', - 'Wagon', - 'Waist', - 'Waiter', - 'Wake', - 'Walk', - 'Wall', - 'Wallet', - 'Walnut', - 'Waltz', - 'War', - 'Ward', - 'Warehouse', - 'Warmth', - 'Warning', - 'Warrant', - 'Warrior', - 'Wash', - 'Washer', - 'Waste', - 'Watch', - 'Water', - 'Waterfall', - 'Wave', - 'Wavelength', - 'Wax', - 'Way', - 'Weakness', - 'Wealth', - 'Weapon', - 'Weather', - 'Web', - 'Wedding', - 'Wedge', - 'Week', - 'Weekend', - 'Weight', - 'Welcome', - 'Welfare', - 'Well', - 'West', - 'Whale', - 'Wheat', - 'Wheel', - 'Whiskey', - 'Whisper', - 'Whistle', - 'White', - 'Whole', - 'Wicket', - 'Widow', - 'Wife', - 'Wild', - 'Wilderness', - 'Wildlife', - 'Will', - 'Willow', - 'Win', - 'Wind', - 'Windmill', - 'Window', - 'Wine', - 'Wing', - 'Winner', - 'Winter', - 'Wire', - 'Wisdom', - 'Wish', - 'Witness', - 'Wolf', - 'Woman', - 'Wonder', - 'Wood', - 'Wool', - 'Word', - 'Work', - 'Worker', - 'World', - 'Worm', - 'Worry', - 'Worship', - 'Worth', - 'Wound', - 'Wrap', - 'Wreck', - 'Wrestler', - 'Wrinkle', - 'Wrist', - 'Writer', - 'Writing', - 'Xenon', - 'X-ray', - 'Xylophone', - 'Yacht', - 'Yak', - 'Yam', - 'Yard', - 'Yarn', - 'Yawn', - 'Year', - 'Yeast', - 'Yellow', - 'Yen', - 'Yoga', - 'Yogurt', - 'Yolk', - 'Young', - 'Youth', - 'Yukon', - 'Zebra', - 'Zenith', - 'Zephyr', - 'Zero', - 'Zigzag', - 'Zinc', - 'Zinnia', - 'Zip', - 'Zone', - 'Zoo', - 'Zoology', - 'Zoom' -] diff --git a/src/wordlist.ts b/src/wordlist.ts new file mode 100644 index 0000000..cf7f104 --- /dev/null +++ b/src/wordlist.ts @@ -0,0 +1,2640 @@ +export default [ + "Abacus", + "Accordion", + "Acorn", + "Acrobat", + "Adult", + "Aeroplane", + "Air", + "Airplane", + "Alarm", + "Albatross", + "Algae", + "Almond", + "Alphabet", + "Ambulance", + "America", + "Anatomy", + "Anchor", + "Andromeda", + "Angel", + "Ant", + "Apartment", + "Apology", + "Apparatus", + "Apparel", + "Appeal", + "Appendix", + "Apple", + "Apricot", + "Aquarium", + "Arc", + "Archipelago", + "Architecture", + "Argument", + "Armadillo", + "Armchair", + "Armor", + "Army", + "Arrow", + "Artichoke", + "Artist", + "Astronaut", + "Asylum", + "Athlete", + "Atmosphere", + "Atom", + "Audience", + "Aurora", + "Author", + "Automobile", + "Autumn", + "Avalanche", + "Avenue", + "Avocado", + "Award", + "Awkwardness", + "Axe", + "Baboon", + "Baby", + "Backbone", + "Backpack", + "Bacon", + "Badger", + "Bag", + "Baggage", + "Bagpipe", + "Bail", + "Bait", + "Baker", + "Balance", + "Balcony", + "Ball", + "Balloon", + "Ballot", + "Bamboo", + "Banana", + "Bandana", + "Bandit", + "Bangle", + "Banyan", + "Bar", + "Barbecue", + "Barber", + "Bargain", + "Bark", + "Barley", + "Barn", + "Barrel", + "Basement", + "Basket", + "Basketball", + "Bassoon", + "Bat", + "Bath", + "Battery", + "Battle", + "Bay", + "Beach", + "Bead", + "Beak", + "Beam", + "Bean", + "Bear", + "Beard", + "Beast", + "Beat", + "Beauty", + "Beaver", + "Bed", + "Bedroom", + "Bee", + "Beech", + "Beef", + "Beer", + "Beetle", + "Beggar", + "Beginner", + "Beginning", + "Behavior", + "Bell", + "Bellows", + "Belt", + "Bench", + "Bicycle", + "Bigfoot", + "Bike", + "Bill", + "Bird", + "Birth", + "Biscuit", + "Bit", + "Blackbird", + "Blackboard", + "Blackcurrant", + "Bladder", + "Blade", + "Blanket", + "Blazer", + "Blizzard", + "Block", + "Blood", + "Blouse", + "Blueberry", + "Boar", + "Board", + "Boat", + "Body", + "Bologna", + "Bolt", + "Bomb", + "Bone", + "Bonfire", + "Bonnet", + "Book", + "Bookcase", + "Boomerang", + "Boot", + "Border", + "Bottle", + "Bottom", + "Boulder", + "Bouquet", + "Bow", + "Bowl", + "Bowler", + "Box", + "Boy", + "Brain", + "Brake", + "Branch", + "Brass", + "Bread", + "Breast", + "Breath", + "Breeze", + "Brick", + "Bridge", + "Briefcase", + "Brilliance", + "Broccoli", + "Bronze", + "Brother", + "Brownie", + "Brush", + "Bubble", + "Bucket", + "Buffalo", + "Bug", + "Building", + "Bulb", + "Bull", + "Bumblebee", + "Bunker", + "Bureau", + "Burger", + "Burglary", + "Bus", + "Bush", + "Business", + "Bust", + "Butter", + "Butterfly", + "Button", + "Buzzard", + "Cabbage", + "Cabin", + "Cabinet", + "Cable", + "Cactus", + "Cadet", + "Cafe", + "Cage", + "Cake", + "Calculator", + "Calf", + "Calm", + "Camel", + "Camera", + "Camp", + "Can", + "Canal", + "Candle", + "Candy", + "Cane", + "Cannibal", + "Cannon", + "Canoe", + "Canyon", + "Cap", + "Cape", + "Capital", + "Capitalism", + "Capitol", + "Captain", + "Car", + "Caravan", + "Carbon", + "Card", + "Cardboard", + "Care", + "Career", + "Cargo", + "Carpet", + "Carrot", + "Cart", + "Carton", + "Cartoon", + "Case", + "Cash", + "Castle", + "Cat", + "Caterpillar", + "Cathedral", + "Cattle", + "Cave", + "Cavern", + "CD", + "Cedar", + "Cell", + "Cellar", + "Cello", + "Cellophane", + "Cement", + "Cemetery", + "Cereal", + "Chain", + "Chair", + "Chalk", + "Challenge", + "Chamber", + "Champagne", + "Champion", + "Chance", + "Channel", + "Chaos", + "Chapel", + "Chapter", + "Character", + "Charity", + "Charm", + "Chart", + "Chase", + "Chateau", + "Cheese", + "Cheetah", + "Chef", + "Chemistry", + "Cherry", + "Chess", + "Chest", + "Chicken", + "Child", + "Chimpanzee", + "Chin", + "Chip", + "Chocolate", + "Choice", + "Choir", + "Chowder", + "Christmas", + "Chrome", + "Church", + "Cider", + "Cigar", + "Cigarette", + "Cinema", + "Circle", + "Circus", + "City", + "Clam", + "Clarity", + "Class", + "Classic", + "Classroom", + "Claw", + "Clay", + "Cleaner", + "Clearing", + "Cleavage", + "Clerk", + "Cliff", + "Climate", + "Climax", + "Climbing", + "Clinic", + "Clip", + "Clock", + "Closet", + "Cloth", + "Clothes", + "Cloud", + "Clover", + "Club", + "Coach", + "Coal", + "Coast", + "Coat", + "Cobra", + "Cobweb", + "Cocktail", + "Cocoa", + "Coffee", + "Coffin", + "Coin", + "Cola", + "Cold", + "Collar", + "Collection", + "College", + "Colony", + "Color", + "Column", + "Comb", + "Comet", + "Comfort", + "Comic", + "Command", + "Comment", + "Commerce", + "Committee", + "Common", + "Communication", + "Community", + "Company", + "Compass", + "Competition", + "Composer", + "Compromise", + "Computer", + "Concentration", + "Concept", + "Concern", + "Concert", + "Conclusion", + "Concrete", + "Condition", + "Conduct", + "Cone", + "Conference", + "Confession", + "Confidence", + "Conflict", + "Confusion", + "Congo", + "Congress", + "Conifer", + "Connection", + "Consequence", + "Conservatory", + "Consideration", + "Consolation", + "Conspiracy", + "Constant", + "Construction", + "Consul", + "Container", + "Content", + "Contention", + "Continent", + "Contract", + "Control", + "Cook", + "Cooperation", + "Copper", + "Copy", + "Coral", + "Cord", + "Cork", + "Corn", + "Corner", + "Coronation", + "Corporation", + "Corridor", + "Corruption", + "Cosmos", + "Cost", + "Costume", + "Cottage", + "Cotton", + "Cough", + "Council", + "Count", + "Counter", + "Country", + "Courage", + "Course", + "Court", + "Cousin", + "Covenant", + "Cover", + "Cow", + "Cowboy", + "Crab", + "Cracker", + "Cradle", + "Craft", + "Crane", + "Crate", + "Crater", + "Crayon", + "Cream", + "Creativity", + "Creature", + "Creek", + "Crepe", + "Crib", + "Cricket", + "Crime", + "Crisis", + "Criterion", + "Criticism", + "Crocodile", + "Crop", + "Cross", + "Crow", + "Crown", + "Cruiser", + "Crumb", + "Crush", + "Cry", + "Crystal", + "Cube", + "Cuckoo", + "Cucumber", + "Cult", + "Culture", + "Cup", + "Cupboard", + "Curiosity", + "Curtain", + "Cushion", + "Custom", + "Cut", + "Cutter", + "Cycle", + "Cylinder", + "Cymbal", + "Cypress", + "Daddy", + "Daffodil", + "Dagger", + "Dahlia", + "Dairy", + "Dam", + "Damage", + "Dance", + "Danger", + "Dare", + "Darkness", + "Dart", + "Dash", + "Database", + "Date", + "Daughter", + "Dawn", + "Day", + "Daylight", + "Deacon", + "Deal", + "Dean", + "Death", + "Debate", + "Debt", + "Decade", + "Decay", + "Deception", + "Decision", + "Deck", + "Decline", + "Decoration", + "Decree", + "Deer", + "Defence", + "Deficit", + "Definition", + "Degree", + "Delay", + "Delegation", + "Delight", + "Delivery", + "Demand", + "Democracy", + "Demon", + "Den", + "Dentist", + "Department", + "Departure", + "Deposit", + "Depression", + "Depth", + "Deputy", + "Desert", + "Desire", + "Desk", + "Despair", + "Dessert", + "Destination", + "Detail", + "Detective", + "Determinant", + "Development", + "Device", + "Devotion", + "Dew", + "Diagram", + "Dialect", + "Diamond", + "Diaphragm", + "Diary", + "Dictator", + "Dictionary", + "Dinosaur", + "Diploma", + "Direction", + "Director", + "Dirt", + "Disability", + "Disadvantage", + "Disaster", + "Disc", + "Discipline", + "Disclosure", + "Discount", + "Discovery", + "Discussion", + "Disease", + "Dish", + "Disk", + "Disposal", + "Distance", + "Distinction", + "Disturbance", + "Dive", + "Diversity", + "Division", + "Doctor", + "Doctrine", + "Document", + "Dog", + "Dollar", + "Doll", + "Dolphin", + "Donation", + "Donkey", + "Door", + "Doorknob", + "Dot", + "Doubt", + "Dough", + "Dove", + "Downpour", + "Dragon", + "Drain", + "Drama", + "Drawer", + "Dream", + "Dressing", + "Drill", + "Drink", + "Drive", + "Driver", + "Driving", + "Drizzle", + "Drone", + "Drop", + "Drum", + "Duck", + "Dune", + "Dungeon", + "Durian", + "Dust", + "Duty", + "Dwarf", + "Dye", + "Eagle", + "Ear", + "Earth", + "Earthquake", + "Ease", + "East", + "Echo", + "Economy", + "Edge", + "Edifice", + "Editor", + "Education", + "Effect", + "Effort", + "Egg", + "Eggplant", + "Ego", + "Elbow", + "Election", + "Electricity", + "Elephant", + "Elevator", + "Elite", + "Elk", + "Embarrassment", + "Embrace", + "Emerald", + "Emotion", + "Emperor", + "Emphasis", + "Empire", + "Employee", + "Employer", + "Employment", + "End", + "Endurance", + "Energy", + "Engine", + "Engineer", + "Enigma", + "Enjoyment", + "Enough", + "Enquiry", + "Enterprise", + "Entertainment", + "Enthusiasm", + "Entrance", + "Environment", + "Episode", + "Equality", + "Equation", + "Equinox", + "Equipment", + "Era", + "Eruption", + "Escalator", + "Escape", + "Espresso", + "Essence", + "Estate", + "Estimate", + "Eternity", + "Ethics", + "Europe", + "Evaluation", + "Eve", + "Evening", + "Event", + "Evidence", + "Evil", + "Evolution", + "Example", + "Excavation", + "Excellence", + "Exception", + "Exchange", + "Excitement", + "Excuse", + "Execution", + "Executive", + "Exercise", + "Exhibition", + "Existence", + "Exit", + "Expansion", + "Expectation", + "Expedition", + "Expense", + "Experience", + "Experiment", + "Expert", + "Explanation", + "Exploration", + "Explosion", + "Exposure", + "Extension", + "Extent", + "External", + "Eye", + "Eyeball", + "Eyebrow", + "Eyelash", + "Eyelid", + "Fabric", + "Face", + "Facility", + "Fact", + "Factor", + "Faculty", + "Fading", + "Failure", + "Fair", + "Faith", + "Falcon", + "Fall", + "Family", + "Fan", + "Fantasy", + "Farm", + "Farmer", + "Fascination", + "Fashion", + "Fate", + "Father", + "Fatigue", + "Fault", + "Fear", + "Feast", + "Feather", + "Feature", + "Federation", + "Fee", + "Feedback", + "Feeling", + "Feet", + "Fence", + "Ferret", + "Festival", + "Fiber", + "Fiction", + "Fiddle", + "Field", + "Fig", + "Fight", + "Figure", + "File", + "Film", + "Filter", + "Fin", + "Finance", + "Finding", + "Finger", + "Finish", + "Fire", + "Firefighter", + "Firefly", + "Firm", + "Fish", + "Fisherman", + "Fitness", + "Fix", + "Fixture", + "Flag", + "Flame", + "Flamingo", + "Flash", + "Flax", + "Flea", + "Fleet", + "Flesh", + "Flight", + "Flock", + "Flood", + "Floor", + "Flour", + "Flower", + "Fluid", + "Flute", + "Fly", + "Foam", + "Fog", + "Foil", + "Folder", + "Folk", + "Food", + "Foot", + "Football", + "Force", + "Forecast", + "Forehead", + "Forest", + "Fork", + "Form", + "Format", + "Former", + "Fort", + "Fortune", + "Foundation", + "Fountain", + "Fox", + "Fraction", + "Fragment", + "Frame", + "France", + "Freedom", + "Freeze", + "Freezer", + "Freight", + "French", + "Frequency", + "Friction", + "Friend", + "Frog", + "Front", + "Frost", + "Frown", + "Fruit", + "Fuel", + "Function", + "Funding", + "Funeral", + "Fur", + "Furnace", + "Furniture", + "Future", + "Gadget", + "Gaffer", + "Gain", + "Gait", + "Galaxy", + "Gallery", + "Galleon", + "Gallon", + "Game", + "Garden", + "Garlic", + "Garment", + "Gas", + "Gate", + "Gathering", + "Gauge", + "Gazelle", + "Gear", + "Geese", + "Gem", + "Gender", + "Gene", + "General", + "Genius", + "Genre", + "Gentleman", + "Geography", + "Geology", + "Geometry", + "Geranium", + "Germany", + "Gesture", + "Ghost", + "Giant", + "Gift", + "Giraffe", + "Girl", + "Glacier", + "Gladness", + "Glass", + "Glaze", + "Glimpse", + "Globe", + "Gloom", + "Glory", + "Glove", + "Glow", + "Glue", + "Goal", + "Goat", + "God", + "Gold", + "Goldfish", + "Golf", + "Gondola", + "Gong", + "Good", + "Goodness", + "Goose", + "Gorilla", + "Gossip", + "Government", + "Governor", + "Gown", + "Grace", + "Grade", + "Grain", + "Grammar", + "Grandchild", + "Grandfather", + "Grandmother", + "Grandparent", + "Grandson", + "Grape", + "Graph", + "Grass", + "Grasshopper", + "Gratitude", + "Grave", + "Gravity", + "Grease", + "Greatness", + "Greenhouse", + "Greeting", + "Grid", + "Grief", + "Grille", + "Grimace", + "Grind", + "Grit", + "Grocery", + "Ground", + "Group", + "Grouse", + "Growth", + "Guarantee", + "Guard", + "Guess", + "Guest", + "Guidance", + "Guide", + "Guitar", + "Gum", + "Gun", + "Gutter", + "Gym", + "Habitat", + "Hair", + "Haircut", + "Hall", + "Hamburger", + "Hammer", + "Hamster", + "Hand", + "Handbag", + "Handicap", + "Handle", + "Handsaw", + "Happiness", + "Harbor", + "Hardship", + "Hare", + "Harm", + "Harmony", + "Harp", + "Harvest", + "Hat", + "Hate", + "Hawk", + "Hazard", + "Head", + "Headache", + "Headland", + "Headlight", + "Headline", + "Headquarters", + "Health", + "Hearing", + "Heart", + "Heat", + "Heather", + "Heaven", + "Hedge", + "Hedgehog", + "Height", + "Helicopter", + "Hell", + "Helmet", + "Help", + "Hemisphere", + "Herb", + "Heron", + "Hero", + "Herring", + "Hesitation", + "Hide", + "Highway", + "Hill", + "Hip", + "Hippopotamus", + "Hire", + "Historian", + "History", + "Hobbit", + "Hockey", + "Hold", + "Hole", + "Holiday", + "Holland", + "Home", + "Honey", + "Honeydew", + "Honesty", + "Honor", + "Hood", + "Hoof", + "Hook", + "Hope", + "Horizon", + "Horn", + "Horse", + "Hospital", + "Host", + "Hostel", + "Hotdog", + "Hotel", + "Hour", + "House", + "Hovercraft", + "Hummingbird", + "Humor", + "Hunter", + "Hurricane", + "Hurry", + "Husband", + "Hut", + "Hyacinth", + "Hydrant", + "Hyena", + "Hygiene", + "Hypothesis", + "Ice", + "Iceberg", + "Icicle", + "Idea", + "Ideal", + "Identity", + "Ideology", + "Idiot", + "Igloo", + "Ignorance", + "Illusion", + "Image", + "Imagination", + "Impact", + "Implement", + "Importance", + "Impression", + "Improvement", + "Incense", + "Incentive", + "Inch", + "Incidence", + "Income", + "Increase", + "Independence", + "Index", + "India", + "Indication", + "Individual", + "Industry", + "Infant", + "Infection", + "Influence", + "Information", + "Ingenuity", + "Ingredient", + "Inhabitant", + "Inheritance", + "Initial", + "Initiative", + "Injection", + "Injury", + "Ink", + "Inquiry", + "Insect", + "Inside", + "Insight", + "Insistence", + "Inspection", + "Inspector", + "Inspiration", + "Installment", + "Instance", + "Instinct", + "Institute", + "Institution", + "Instruction", + "Instrument", + "Insulation", + "Insurance", + "Intelligence", + "Intention", + "Interest", + "Interference", + "Interior", + "Internet", + "Interpretation", + "Interruption", + "Interview", + "Introduction", + "Intuition", + "Invention", + "Inventor", + "Investment", + "Invitation", + "Invoice", + "Iron", + "Island", + "Issue", + "Italy", + "Item", + "Jackal", + "Jacket", + "Jaguar", + "Jail", + "Jalapeño", + "Jam", + "Jar", + "Jasmine", + "Javelin", + "Jaw", + "Jeans", + "Jelly", + "Jellyfish", + "Jet", + "Jewel", + "Jewelry", + "Job", + "Jockey", + "Joker", + "Journey", + "Joy", + "Judge", + "Judgment", + "Juice", + "Jumper", + "Junction", + "Jungle", + "Juniper", + "Junk", + "Justice", + "Kangaroo", + "Karate", + "Kayaking", + "Ketchup", + "Kettle", + "Key", + "Keyboard", + "Kick", + "Kid", + "Kidney", + "Kilometer", + "King", + "Kingdom", + "Kiss", + "Kitchen", + "Kite", + "Kitten", + "Knapsack", + "Knee", + "Kneel", + "Knife", + "Knight", + "Knitting", + "Knob", + "Knock", + "Knot", + "Knowledge", + "Koala", + "Lab", + "Label", + "Labor", + "Laboratory", + "Lace", + "Lack", + "Ladder", + "Lad", + "Lady", + "Lake", + "Lamb", + "Lamp", + "Lancet", + "Land", + "Landscape", + "Lane", + "Language", + "Lantern", + "Lap", + "Lapel", + "Larch", + "Lard", + "Lark", + "Laughter", + "Launch", + "Lavender", + "Law", + "Lawn", + "Lawyer", + "Lead", + "Leader", + "Leaf", + "League", + "Leak", + "Leather", + "Lecture", + "Leg", + "Legacy", + "Legal", + "Legend", + "Legislation", + "Leisure", + "Lemon", + "Length", + "Lentil", + "Leopard", + "Letter", + "Lettuce", + "Level", + "Lever", + "Liability", + "Library", + "License", + "Lid", + "Life", + "Lift", + "Light", + "Lightning", + "Lilac", + "Lily", + "Limb", + "Lime", + "Limit", + "Line", + "Linen", + "Link", + "Lion", + "Lip", + "Lipstick", + "Liquid", + "List", + "Listen", + "Literacy", + "Literature", + "Litter", + "Liver", + "Lizard", + "Llama", + "Loaf", + "Lobby", + "Lobster", + "Local", + "Location", + "Lock", + "Locker", + "Locust", + "Loft", + "Log", + "Logic", + "Lollipop", + "Loneliness", + "Longboat", + "Look", + "Loop", + "Lord", + "Loss", + "Lot", + "Lotion", + "Lounge", + "Love", + "Lover", + "Loyalty", + "Luck", + "Luggage", + "Lullaby", + "Lunch", + "Lungs", + "Lute", + "Luxury", + "Lynx", + "Lyre", + "Macaroon", + "Machine", + "Madness", + "Magazine", + "Magic", + "Magnet", + "Magnitude", + "Maiden", + "Mail", + "Mailbox", + "Mainland", + "Maintenance", + "Majority", + "Make", + "Maker", + "Malaria", + "Male", + "Mall", + "Man", + "Management", + "Manager", + "Mango", + "Mansion", + "Manual", + "Manufacture", + "Map", + "Maple", + "Marble", + "March", + "Mare", + "Margin", + "Mark", + "Market", + "Marketing", + "Marmalade", + "Marriage", + "Marsh", + "Marshmallow", + "Mask", + "Mass", + "Master", + "Match", + "Material", + "Math", + "Matter", + "Mattress", + "Maximum", + "Mayonnaise", + "Maze", + "Meadow", + "Meal", + "Meaning", + "Measure", + "Meat", + "Mechanic", + "Mechanism", + "Medal", + "Medicine", + "Mediocrity", + "Meeting", + "Melody", + "Melon", + "Member", + "Memo", + "Memory", + "Men", + "Mention", + "Menu", + "Mercy", + "Mess", + "Message", + "Metal", + "Meter", + "Method", + "Mexico", + "Microphone", + "Microscope", + "Midday", + "Middle", + "Midnight", + "Might", + "Migration", + "Milk", + "Mill", + "Millennium", + "Mind", + "Mine", + "Mineral", + "Minibus", + "Minimum", + "Minister", + "Minority", + "Mint", + "Minute", + "Mirror", + "Missile", + "Mission", + "Mist", + "Mistake", + "Mister", + "Mitten", + "Mix", + "Mixture", + "Mobile", + "Mode", + "Model", + "Modesty", + "Mole", + "Moment", + "Monarch", + "Money", + "Monitor", + "Monkey", + "Monocle", + "Monster", + "Month", + "Monument", + "Mood", + "Moon", + "Moose", + "Moral", + "Morning", + "Moth", + "Mother", + "Motion", + "Motivation", + "Motor", + "Motorcycle", + "Mound", + "Mountain", + "Mouse", + "Mouth", + "Movement", + "Movie", + "Muffin", + "Mule", + "Multiple", + "Mummy", + "Muscle", + "Museum", + "Mushroom", + "Music", + "Musician", + "Mustard", + "Mystery", + "Myth", + "Nail", + "Name", + "Napkin", + "Nation", + "Nature", + "Navel", + "Navigation", + "Nebula", + "Neck", + "Necklace", + "Need", + "Needle", + "Negative", + "Negotiation", + "Neighbor", + "Neighborhood", + "Nerve", + "Nest", + "Net", + "Network", + "News", + "Newspaper", + "Niche", + "Night", + "Nightmare", + "Nightingale", + "Noise", + "Noodle", + "North", + "Nose", + "Note", + "Notebook", + "Nothing", + "Notice", + "Notion", + "Novel", + "Novelty", + "Nuisance", + "Number", + "Nurse", + "Nut", + "Nutrition", + "Nylon", + "Oak", + "Oar", + "Oasis", + "Objection", + "Objective", + "Obligation", + "Observation", + "Observer", + "Obstacle", + "Occasion", + "Occupation", + "Ocean", + "Ocelot", + "Octopus", + "Offence", + "Offer", + "Office", + "Officer", + "Official", + "Oil", + "Ointment", + "Old", + "Olive", + "Omnibus", + "Onion", + "Open", + "Opening", + "Opera", + "Operation", + "Opinion", + "Opportunity", + "Opponent", + "Optics", + "Option", + "Orange", + "Orchestra", + "Order", + "Organ", + "Organization", + "Orientation", + "Origin", + "Ornament", + "Ostrich", + "Otter", + "Outcome", + "Outfit", + "Outlet", + "Outline", + "Output", + "Oval", + "Oven", + "Overalls", + "Overcoat", + "Overhang", + "Overload", + "Owl", + "Owner", + "Oxygen", + "Oyster", + "Pace", + "Package", + "Packet", + "Pad", + "Page", + "Pain", + "Paint", + "Painting", + "Pair", + "Palace", + "Paleontologist", + "Palm", + "Pan", + "Panda", + "Panel", + "Panic", + "Pants", + "Paper", + "Paperback", + "Parade", + "Paradise", + "Paragraph", + "Parallel", + "Parasite", + "Parent", + "Park", + "Parrot", + "Part", + "Partner", + "Party", + "Passage", + "Past", + "Pastor", + "Patch", + "Path", + "Patience", + "Patient", + "Patio", + "Patrol", + "Pattern", + "Pause", + "Pavement", + "Paw", + "Payment", + "Pea", + "Peace", + "Peach", + "Peak", + "Pear", + "Pearl", + "Peasant", + "Pebble", + "Pedal", + "Pedestrian", + "Peel", + "Pen", + "Penalty", + "Pencil", + "Penguin", + "Pension", + "People", + "Pepper", + "Percentage", + "Perception", + "Performance", + "Period", + "Permission", + "Permit", + "Persistence", + "Person", + "Personality", + "Perspective", + "Pet", + "Petition", + "Phase", + "Pheasant", + "Phenomenon", + "Philosophy", + "Phone", + "Photo", + "Photography", + "Phrase", + "Physics", + "Piano", + "Pickle", + "Picture", + "Pie", + "Piece", + "Pig", + "Pigeon", + "Pigment", + "Pile", + "Pill", + "Pillar", + "Pillow", + "Pilot", + "Pine", + "Pineapple", + "Pink", + "Pint", + "Pipe", + "Pistol", + "Pit", + "Pitch", + "Pizza", + "Place", + "Plain", + "Plan", + "Plane", + "Planet", + "Plant", + "Plastic", + "Plate", + "Platform", + "Play", + "Player", + "Playground", + "Pleasure", + "Pledge", + "Plot", + "Plug", + "Plum", + "Plumber", + "Pocket", + "Poem", + "Poet", + "Point", + "Poison", + "Pole", + "Police", + "Policy", + "Politeness", + "Poll", + "Pollen", + "Polo", + "Pond", + "Pony", + "Pool", + "Popcorn", + "Pope", + "Poplar", + "Poppy", + "Popularity", + "Population", + "Porcelain", + "Porch", + "Porcupine", + "Port", + "Porter", + "Portfolio", + "Portion", + "Portrait", + "Position", + "Possession", + "Possibility", + "Post", + "Postage", + "Poster", + "Pot", + "Potato", + "Potential", + "Pound", + "Poverty", + "Powder", + "Power", + "Practice", + "Praise", + "Pray", + "Prayer", + "Preacher", + "Precedence", + "Precision", + "Prediction", + "Preference", + "Preparation", + "Presence", + "Present", + "Presentation", + "Preservation", + "President", + "Press", + "Pressure", + "Prestige", + "Prevention", + "Price", + "Pride", + "Priest", + "Primary", + "Prime", + "Prince", + "Princess", + "Principal", + "Principle", + "Print", + "Prior", + "Priority", + "Prison", + "Prisoner", + "Privacy", + "Prize", + "Problem", + "Procedure", + "Process", + "Prodigy", + "Produce", + "Product", + "Profession", + "Professor", + "Profit", + "Program", + "Progress", + "Project", + "Promise", + "Promotion", + "Proof", + "Property", + "Proposal", + "Prosecution", + "Prospect", + "Protection", + "Protest", + "Proud", + "Province", + "Provision", + "Psychology", + "Pub", + "Public", + "Publication", + "Publicity", + "Publisher", + "Pudding", + "Puddle", + "Puffin", + "Pull", + "Pulpit", + "Pulse", + "Puma", + "Pump", + "Pumpkin", + "Punch", + "Punctuation", + "Punishment", + "Puppet", + "Puppy", + "Purchase", + "Purpose", + "Purse", + "Pursuit", + "Push", + "Put", + "Pyramid", + "Quadrant", + "Quail", + "Quality", + "Quantity", + "Quartz", + "Queen", + "Quill", + "Quilt", + "Quince", + "Quit", + "Rabbit", + "Raccoon", + "Race", + "Rack", + "Radar", + "Radiator", + "Radio", + "Radish", + "Raft", + "Rag", + "Rail", + "Rain", + "Rainbow", + "Raincoat", + "Ram", + "Range", + "Rank", + "Rapids", + "Rate", + "Raven", + "Raw", + "Ray", + "Razor", + "Reaction", + "Reader", + "Reading", + "Reality", + "Reason", + "Reception", + "Recipe", + "Recognition", + "Recommendation", + "Record", + "Recording", + "Recovery", + "Recreation", + "Rectangle", + "Redcurrant", + "Redoubt", + "Reed", + "Reference", + "Refinement", + "Reflection", + "Reform", + "Refusal", + "Regard", + "Regret", + "Regular", + "Regulation", + "Rein", + "Reindeer", + "Relation", + "Relationship", + "Relaxation", + "Release", + "Relief", + "Religion", + "Remark", + "Remedy", + "Reminder", + "Removal", + "Rent", + "Repair", + "Repeat", + "Replacement", + "Reply", + "Report", + "Reporter", + "Representation", + "Representative", + "Reproduction", + "Reputation", + "Request", + "Requirement", + "Research", + "Reserve", + "Resident", + "Resistance", + "Resolution", + "Resort", + "Resource", + "Respect", + "Response", + "Responsibility", + "Rest", + "Restaurant", + "Result", + "Retail", + "Retirement", + "Return", + "Revelation", + "Revenge", + "Revenue", + "Reverse", + "Review", + "Revolution", + "Reward", + "Rhetoric", + "Rhinoceros", + "Rhythm", + "Rib", + "Ribbon", + "Rice", + "Riddle", + "Ridge", + "Rifle", + "Right", + "Ring", + "Rip", + "Rise", + "Risk", + "Ritual", + "River", + "Road", + "Roadrunner", + "Roast", + "Robbery", + "Robot", + "Rock", + "Rocket", + "Rodent", + "Role", + "Roll", + "Romance", + "Roof", + "Room", + "Rooster", + "Root", + "Rope", + "Rose", + "Rotation", + "Rough", + "Round", + "Route", + "Routine", + "Row", + "Royal", + "Rubbish", + "Ruby", + "Rucksack", + "Ruin", + "Rule", + "Ruler", + "Rumor", + "Run", + "Runner", + "Rush", + "Rust", + "Rye", + "Sack", + "Sacrifice", + "Sadness", + "Saddle", + "Safari", + "Safety", + "Saffron", + "Sage", + "Sail", + "Sailor", + "Salad", + "Salary", + "Sale", + "Salmon", + "Salt", + "Sample", + "Sand", + "Sandal", + "Sandwich", + "Sapphire", + "Satellite", + "Satisfaction", + "Sauce", + "Saucepan", + "Sausage", + "Save", + "Saw", + "Scale", + "Scandal", + "Scarecrow", + "Scarf", + "Scene", + "Schedule", + "Scheme", + "Scholar", + "Scholarship", + "School", + "Science", + "Scientist", + "Scoop", + "Scope", + "Score", + "Scorpion", + "Scout", + "Scrabble", + "Scramble", + "Scrap", + "Scratch", + "Screen", + "Screw", + "Script", + "Sculpture", + "Sea", + "Seagull", + "Seal", + "Search", + "Seashore", + "Season", + "Seat", + "Secret", + "Secretary", + "Section", + "Sector", + "Security", + "Seed", + "Seek", + "Selection", + "Self", + "Seller", + "Sense", + "Sensitivity", + "Sentence", + "Separation", + "September", + "Sequence", + "Series", + "Serve", + "Server", + "Service", + "Session", + "Set", + "Setting", + "Settle", + "Settlement", + "Shade", + "Shadow", + "Shake", + "Shame", + "Shampoo", + "Shape", + "Share", + "Shark", + "Sheep", + "Sheet", + "Shelf", + "Shell", + "Shelter", + "Shepherd", + "Shield", + "Shift", + "Shine", + "Ship", + "Shirt", + "Shock", + "Shoe", + "Shooting", + "Shop", + "Shopping", + "Shore", + "Short", + "Shortage", + "Shot", + "Shoulder", + "Shout", + "Show", + "Shower", + "Shrimp", + "Shrine", + "Sibling", + "Sick", + "Side", + "Sidewalk", + "Sight", + "Sign", + "Signal", + "Signature", + "Significance", + "Silence", + "Silk", + "Silver", + "Similarity", + "Simon", + "Simple", + "Simplicity", + "Sin", + "Singer", + "Single", + "Sink", + "Sister", + "Site", + "Situation", + "Size", + "Sketch", + "Ski", + "Skill", + "Skin", + "Skirt", + "Skull", + "Sky", + "Slab", + "Slang", + "Slave", + "Slavery", + "Sleep", + "Sleeve", + "Slice", + "Slide", + "Slip", + "Slope", + "Slot", + "Sly", + "Smell", + "Smile", + "Smoke", + "Snail", + "Snake", + "Snow", + "Soap", + "Society", + "Sock", + "Soda", + "Softness", + "Soil", + "Solar", + "Soldier", + "Sole", + "Solicitor", + "Solid", + "Solution", + "Somersault", + "Son", + "Song", + "Soprano", + "Sore", + "Sort", + "Soul", + "Sound", + "Soup", + "Source", + "South", + "Sovereignty", + "Space", + "Spade", + "Spaghetti", + "Spain", + "Span", + "Sparrow", + "Speaker", + "Special", + "Specialist", + "Species", + "Specification", + "Speech", + "Speed", + "Spell", + "Spice", + "Spider", + "Spike", + "Spinach", + "Spirit", + "Spite", + "Splash", + "Spleen", + "Splendor", + "Split", + "Spoon", + "Sport", + "Spot", + "Spring", + "Squad", + "Square", + "Squash", + "Squirrel", + "Stability", + "Stadium", + "Staff", + "Stage", + "Stain", + "Stair", + "Stake", + "Stall", + "Stamp", + "Stance", + "Stand", + "Standard", + "Star", + "Stare", + "Start", + "State", + "Statement", + "Station", + "Statistic", + "Status", + "Steak", + "Steam", + "Steel", + "Steering", + "Step", + "Stick", + "Sticker", + "Stomach", + "Stone", + "Stool", + "Stop", + "Storage", + "Store", + "Storm", + "Story", + "Stove", + "Strain", + "Strait", + "Strand", + "Stranger", + "Strategy", + "Strawberry", + "Stream", + "Street", + "Strength", + "Stress", + "Stretch", + "Strike", + "String", + "Strip", + "Structure", + "Struggle", + "Student", + "Studio", + "Study", + "Stuff", + "Stupidity", + "Style", + "Subject", + "Submission", + "Substance", + "Success", + "Sugar", + "Suggestion", + "Suit", + "Sulphur", + "Summit", + "Sun", + "Sunshine", + "Supermarket", + "Supply", + "Support", + "Surgeon", + "Surgery", + "Surname", + "Surprise", + "Surrender", + "Surroundings", + "Survey", + "Survival", + "Suspicion", + "Swallow", + "Swan", + "Swear", + "Sweater", + "Sweep", + "Sweet", + "Sweetness", + "Swim", + "Swimming", + "Swine", + "Swing", + "Switch", + "Sword", + "Symbol", + "Sympathy", + "Syrup", + "System", + "Table", + "Tablet", + "Taboo", + "Tackle", + "Tail", + "Tailor", + "Talent", + "Talk", + "Tank", + "Tap", + "Tape", + "Target", + "Task", + "Taste", + "Tattoo", + "Tax", + "Taxi", + "Tea", + "Teacher", + "Team", + "Tear", + "Technique", + "Technology", + "Teeth", + "Telegram", + "Telephone", + "Telescope", + "Television", + "Temper", + "Temperature", + "Temple", + "Tempo", + "Temptation", + "Tendency", + "Tennis", + "Tension", + "Tent", + "Term", + "Territory", + "Terror", + "Test", + "Text", + "Texture", + "Thank", + "Thanks", + "Theme", + "Theory", + "Therapy", + "Thick", + "Thief", + "Thing", + "Think", + "Thinness", + "Thought", + "Thread", + "Threat", + "Throat", + "Throne", + "Thumb", + "Thunder", + "Ticket", + "Tide", + "Tiger", + "Tile", + "Time", + "Tin", + "Tip", + "Tire", + "Tissue", + "Title", + "Toad", + "Toast", + "Tobacco", + "Today", + "Toe", + "Together", + "Toilet", + "Toleration", + "Tomato", + "Tomorrow", + "Tongue", + "Tonight", + "Tool", + "Tooth", + "Toothbrush", + "Toothpaste", + "Top", + "Topic", + "Torch", + "Tornado", + "Tortoise", + "Torture", + "Total", + "Touch", + "Tough", + "Tour", + "Tourism", + "Tourist", + "Towel", + "Tower", + "Town", + "Toxic", + "Toy", + "Trace", + "Track", + "Tractor", + "Trade", + "Tradition", + "Traffic", + "Tragedy", + "Trail", + "Trailer", + "Train", + "Trainer", + "Training", + "Trait", + "Transaction", + "Transcript", + "Transfer", + "Transformation", + "Transition", + "Translation", + "Transmission", + "Transport", + "Trap", + "Travel", + "Tray", + "Treasure", + "Treat", + "Treatment", + "Tree", + "Trial", + "Triangle", + "Tribe", + "Trick", + "Trolley", + "Trouble", + "Trousers", + "Truck", + "Trumpet", + "Trunk", + "Trust", + "Truth", + "Tuba", + "Tube", + "Tulip", + "Tuna", + "Tunnel", + "Turkey", + "Turn", + "Turnip", + "Turtle", + "Twin", + "Twist", + "Type", + "Typhoon", + "Tyre", + "Uganda", + "Ukulele", + "Ultimate", + "Umbrella", + "Uncle", + "Under", + "Underpants", + "Understanding", + "Underwear", + "Unemployment", + "Uniform", + "Union", + "Unit", + "United", + "Unity", + "Universe", + "University", + "Unknown", + "Unrest", + "Uranus", + "Urgent", + "Urine", + "Usage", + "Use", + "User", + "Utility", + "Utopia", + "Vacancy", + "Vacation", + "Vacuum", + "Valley", + "Value", + "Van", + "Vanilla", + "Vanish", + "Variety", + "Vase", + "Vastness", + "Veal", + "Vector", + "Vegetable", + "Vehicle", + "Veil", + "Vein", + "Velvet", + "Vendor", + "Ventilation", + "Venture", + "Verdict", + "Verification", + "Verse", + "Version", + "Vessel", + "Veteran", + "Vet", + "Vibration", + "Vicinity", + "Victim", + "Victory", + "Video", + "View", + "Village", + "Vine", + "Vinegar", + "Violation", + "Violet", + "Violin", + "Viper", + "Virus", + "Visa", + "Vision", + "Visit", + "Visitor", + "Visual", + "Vitality", + "Vitamin", + "Vocation", + "Vodka", + "Voice", + "Volcano", + "Volleyball", + "Volume", + "Volunteer", + "Vomit", + "Vortex", + "Vote", + "Voucher", + "Vowel", + "Voyage", + "Vulture", + "Waddle", + "Wafer", + "Wage", + "Wagon", + "Waist", + "Waiter", + "Wake", + "Walk", + "Wall", + "Wallet", + "Walnut", + "Waltz", + "War", + "Ward", + "Warehouse", + "Warmth", + "Warning", + "Warrant", + "Warrior", + "Wash", + "Washer", + "Waste", + "Watch", + "Water", + "Waterfall", + "Wave", + "Wavelength", + "Wax", + "Way", + "Weakness", + "Wealth", + "Weapon", + "Weather", + "Web", + "Wedding", + "Wedge", + "Week", + "Weekend", + "Weight", + "Welcome", + "Welfare", + "Well", + "West", + "Whale", + "Wheat", + "Wheel", + "Whiskey", + "Whisper", + "Whistle", + "White", + "Whole", + "Wicket", + "Widow", + "Wife", + "Wild", + "Wilderness", + "Wildlife", + "Will", + "Willow", + "Win", + "Wind", + "Windmill", + "Window", + "Wine", + "Wing", + "Winner", + "Winter", + "Wire", + "Wisdom", + "Wish", + "Witness", + "Wolf", + "Woman", + "Wonder", + "Wood", + "Wool", + "Word", + "Work", + "Worker", + "World", + "Worm", + "Worry", + "Worship", + "Worth", + "Wound", + "Wrap", + "Wreck", + "Wrestler", + "Wrinkle", + "Wrist", + "Writer", + "Writing", + "Xenon", + "X-ray", + "Xylophone", + "Yacht", + "Yak", + "Yam", + "Yard", + "Yarn", + "Yawn", + "Year", + "Yeast", + "Yellow", + "Yen", + "Yoga", + "Yogurt", + "Yolk", + "Young", + "Youth", + "Yukon", + "Zebra", + "Zenith", + "Zephyr", + "Zero", + "Zigzag", + "Zinc", + "Zinnia", + "Zip", + "Zone", + "Zoo", + "Zoology", + "Zoom", +]; From a57bc4d2a827df6d8339943f227c131ccad02d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 21:54:46 +0200 Subject: [PATCH 03/22] repository --- src/repository.mjs | 155 --------------------------------------------- src/repository.ts | 148 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 155 deletions(-) delete mode 100644 src/repository.mjs create mode 100644 src/repository.ts diff --git a/src/repository.mjs b/src/repository.mjs deleted file mode 100644 index ebe3313..0000000 --- a/src/repository.mjs +++ /dev/null @@ -1,155 +0,0 @@ -/** -* Mapper function to retrieve ID from item -* @template T,K -* @typedef {function(T): K} RepositoryIdMapper -*/ - -/** -* Function to merge two objects for updating the repository. -* It takes the original and the updated, and returns the merged. -* @template T -* @typedef {function(T, T): T} RepositoryItemMerger -*/ - -export class IdInUseError extends Error { } -export class UnknownItemError extends Error { } - -/** -* Base class for repositories. -* @template T,K -*/ -export class Repository { - /** @type {Map} */ - #items = new Map() - - /** @type {function(T): K} */ - #idMapper - - /** @type {function(T, T): T} */ - #merger - - /** - * Construct repository. - * @param {object} [options] Options - * @param {RepositoryIdMapper} [options.idMapper=idFieldMapper()] Id mapper - * @param {RepositoryItemMerger} [options.itemMerger=assignMerger()] Item merger - */ - constructor (options) { - this.#idMapper = options?.idMapper ?? idFieldMapper() - this.#merger = options?.itemMerger ?? assignMerger() - } - - /** - * Add item to repository. - * @param {T} item Item - * @returns {T} Stored item - * @throws if item id is already in use - */ - add (item) { - const id = this.#idMapper(item) - - if (this.has(id)) { - throw new IdInUseError(`Item already stored with id: ${id}`) - } - - this.#items.set(id, item) - - return item - } - - /** - * Update an existing item. - * @param {T} item Item - * @throws if item id not known - */ - update (item) { - const id = this.#idMapper(item) - - if (!this.has(id)) { - throw new UnknownItemError(`Trying to update unknown item with id: ${id}`) - } - - this.#items.set(id, this.#merger( - this.find(id), - item - )) - } - - /** - * Find item based on id. - * @param {K} id Item id - * @returns {T | undefined} Item or undefined - */ - find (id) { - return this.#items.get(id) - } - - /** - * Check if item with id exists. - * @param {K} id Item id - * @returns {boolean} Whether the item exists - */ - has (id) { - return this.#items.has(id) - } - - /** - * List all items in repository. - * @returns {IterableIterator} Items - */ - list () { - return this.#items.values() - } - - /** - * Remove item by id. - * @param {K} id Item id - * @returns {boolean} Whether any item was deleted - */ - remove (id) { - return this.#items.delete(id) - } - - /** - * Check if item exists. - * @param {T} item Item - * @returns {boolean} Whether the item exists - */ - hasItem (item) { - return this.#items.has(this.#idMapper(item)) - } - - /** - * Remove item. - * @param {T} item Item - * @returns {boolean} Whether any item was deleted - */ - removeItem (item) { - return this.#items.delete(this.#idMapper(item)) - } -} - -/** -* Create an id mapper that grabs the given field of the object. -* @returns {RepositoryIdMapper} -*/ -export function fieldIdMapper (field) { - return v => v[field] -} - -/** -* Create an id mapper that grabs the `id` field of the object. -* @returns {RepositoryIdMapper} -*/ -export function idFieldMapper () { - return v => v.id -} - -/** -* Create an item merger that uses Object.assign to update the object without -* changing the reference. -* @returns {RepositoryItemMerger} -*/ -export function assignMerger () { - return (current, update) => Object.assign(current, update) -} diff --git a/src/repository.ts b/src/repository.ts new file mode 100644 index 0000000..90f652f --- /dev/null +++ b/src/repository.ts @@ -0,0 +1,148 @@ +import assert from "node:assert"; + +export type IdMapper = (item: Partial) => K | undefined; +export type ItemMerger = (a: T, b: Partial) => T; + +export class IdInUseError extends Error { } +export class UnknownItemError extends Error { } + +/** + * Base class for repositories. + */ +export class Repository { + protected items = new Map(); + + constructor( + private getId: IdMapper, + private merge: ItemMerger = (a, b) => ({ ...a, ...b }), + ) { } + + /** + * Add item to repository. + * @throws if item id is already in use + */ + add(item: T): T { + const id = this.requireId(item); + + if (this.has(id)) throw this.idInUseError(id); + + this.items.set(id, item); + return item; + } + + /** + * Update an existing item. + * @throws if item id not known + */ + update(item: Partial) { + const id = this.requireId(item); + const base = this.require(id); + this.items.set(id, this.merge(base, item)); + } + + /** + * Find item based on id. + */ + find(id: K): T | undefined { + return this.items.get(id); + } + + /** + * Return item with id or throw + */ + require(id: K): T { + const item = this.find(id); + if (item === undefined) throw this.notFoundError(id); + + return item; + } + + /** + * Check if item with id exists. + */ + has(id: K): boolean { + return this.items.has(id); + } + + /** + * List all items in repository. + */ + list(): IterableIterator { + return this.items.values(); + } + + /** + * Count all items in repository + */ + count(): number { + return this.items.size; + } + + /** + * Remove item by id. + */ + remove(id: K): boolean { + return this.items.delete(id); + } + + /** + * Check if item exists. + */ + hasItem(item: T): boolean { + return this.items.has(this.requireId(item)); + } + + /** + * Remove item. + */ + removeItem(item: T): boolean { + return this.items.delete(this.requireId(item)); + } + + /** + * Remove all items from the repository + */ + clear() { + this.items.clear(); + } + + protected notFoundError(id: K): Error { + return new UnknownItemError(`No item with ID: ${id}`); + } + + protected idInUseError(id: K): Error { + return new IdInUseError(`ID already in use: ${id}`); + } + + private requireId(item: Partial): K { + const id = this.getId(item); + assert(id !== undefined, "Item has no ID set!"); + return id; + } +} + +/** + * Create an id mapper that grabs the given field of the object. + */ +export function fieldIdMapper, K>( + field: string, +): IdMapper { + return (v) => v[field]; +} + +/** + * Create an id mapper that grabs the `id` field of the object. + */ +export function idFieldMapper(): IdMapper { + return (v) => v.id; +} + +/** + * Create an item merger that uses assignment update the object without changing + * the reference. + */ +export function assignMerger(): ItemMerger { + return (current, update) => { + return { current, ...update } as T; + }; +} From ad2fbb686a05de9cac23b8a8f3e866357b292226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 22:00:07 +0200 Subject: [PATCH 04/22] config --- src/config.mjs | 56 -------------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 src/config.mjs diff --git a/src/config.mjs b/src/config.mjs deleted file mode 100644 index d9b0b60..0000000 --- a/src/config.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import * as dotenv from 'dotenv' -import { boolean, byteSize, duration, integer, number, ports } from './config.parsers.mjs' -import logger, { getLogLevel } from './logger.mjs' -import { urlAlphabet } from 'nanoid' - -dotenv.config() - -const env = process.env - -/** -* Noray configuration type. -*/ -export class NorayConfig { - oid = { - length: integer(env.NORAY_OID_LENGTH) ?? 21, - charset: env.NORAY_OID_CHARSET ?? urlAlphabet - } - - words_oid = { - enabled: boolean(env.NORAY_ENABLE_WORDS_OID) ?? false, - length: integer(env.NORAY_WORDS_OID_LENGTH) ?? 3 - } - - pid = { - length: integer(env.NORAY_PID_LENGTH) ?? 128, - charset: env.NORAY_PID_CHARSET ?? urlAlphabet - } - - socket = { - host: env.NORAY_SOCKET_HOST ?? '0.0.0.0', - port: integer(env.NORAY_SOCKET_PORT) ?? 8890 - } - - http = { - host: env.NORAY_HTTP_HOST ?? '0.0.0.0', - port: integer(env.NORAY_HTTP_PORT) ?? 8891 - } - - udpRelay = { - ports: ports(env.NORAY_UDP_RELAY_PORTS ?? '49152-51200'), - timeout: duration(env.NORAY_UDP_RELAY_TIMEOUT ?? '30s'), - cleanupInterval: duration(env.NORAY_UDP_RELAY_CLEANUP_INTERVAL ?? '30s'), - registrarPort: number(env.NORAY_UDP_REGISTRAR_PORT) ?? 8809, - - maxIndividualTraffic: byteSize(env.NORAY_UDP_RELAY_MAX_INDIVIDUAL_TRAFFIC ?? '128kb'), - maxGlobalTraffic: byteSize(env.NORAY_UDP_RELAY_MAX_GLOBAL_TRAFFIC ?? '1gb'), - trafficInterval: duration(env.NORAY_UDP_RELAY_TRAFFIC_INTERVAL ?? '100ms'), - maxLifetimeDuration: duration(env.NORAY_UDP_RELAY_MAX_LIFETIME_DURATION ?? '4hr'), - maxLifetimeTraffic: byteSize(env.NORAY_UDP_RELAY_MAX_LIFETIME_TRAFFIC ?? '4gb') - } - - loglevel = getLogLevel() -} - -export const config = new NorayConfig() -logger.info({ config }, 'Loaded application config') From 1533ea7e660458a0303a707fab8ec56436bf5896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 22:03:04 +0200 Subject: [PATCH 05/22] assertions --- src/assertions.mjs | 26 -------------- src/assertions.ts | 23 ++++++++++++ src/config.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 26 deletions(-) delete mode 100644 src/assertions.mjs create mode 100644 src/assertions.ts create mode 100644 src/config.ts diff --git a/src/assertions.mjs b/src/assertions.mjs deleted file mode 100644 index 9970a50..0000000 --- a/src/assertions.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { fail } from 'node:assert' - -/** -* Ensure param is present. -* @param {T} value Param value -* @returns {T} Param value -* @throws on missing param -* @template T -*/ -export function requireParam (value) { - return value ?? fail('Mising parameter!') -} - -/** -* Ensure param is a valid enum value. -* @param {T} value Param value -* @param {object} enumDef Enum -* @returns {T} Param value -* @throws on invalid param -* @template T -*/ -export function requireEnum (value, enumDef) { - return Object.values(enumDef).includes(value) - ? value - : fail('Invalid enum value: ' + value) -} diff --git a/src/assertions.ts b/src/assertions.ts new file mode 100644 index 0000000..39bb24b --- /dev/null +++ b/src/assertions.ts @@ -0,0 +1,23 @@ +import { fail } from "node:assert"; + +/** + * Ensure param is present. + * + * @throws on missing value + */ +export function requireParam(value: any) { + return value ?? fail("Mising parameter!"); +} + +/** + * Ensure param is a valid enum value. + * + * The `enumDef` must have a value that is equal to `value`. + * + * @throws on invalid `value` + */ +export function requireEnum(value: T, enumDef: Record) { + return Object.values(enumDef).includes(value) + ? value + : fail("Invalid enum value: " + value); +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..99ca281 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,88 @@ +import * as dotenv from "dotenv"; +import { + boolean, + byteSize, + duration, + integer, + number, + ports, +} from "./config.parsers"; +import logger, { getLogLevel } from "./logger.js"; +import { urlAlphabet } from "nanoid"; + +type ConfigEnv = { [key: string]: string | undefined }; + +export function readConfig(env: ConfigEnv) { + return { + oid: { + length: integer(env.NORAY_OID_LENGTH) ?? 21, + charset: env.NORAY_OID_CHARSET ?? urlAlphabet, + }, + + words_oid: { + enabled: boolean(env.NORAY_ENABLE_WORDS_OID) ?? false, + length: integer(env.NORAY_WORDS_OID_LENGTH) ?? 3, + }, + + pid: { + length: integer(env.NORAY_PID_LENGTH) ?? 128, + charset: env.NORAY_PID_CHARSET ?? urlAlphabet, + }, + + socket: { + host: env.NORAY_SOCKET_HOST ?? "0.0.0.0", + port: integer(env.NORAY_SOCKET_PORT) ?? 8890, + }, + + http: { + host: env.NORAY_HTTP_HOST ?? "0.0.0.0", + port: integer(env.NORAY_HTTP_PORT) ?? 8891, + }, + + udpRelay: { + ports: ports(env.NORAY_UDP_RELAY_PORTS ?? "49152-51200"), + timeout: duration(env.NORAY_UDP_RELAY_TIMEOUT ?? "30s"), + cleanupInterval: duration(env.NORAY_UDP_RELAY_CLEANUP_INTERVAL ?? "30s"), + registrarPort: number(env.NORAY_UDP_REGISTRAR_PORT) ?? 8809, + + maxIndividualTraffic: byteSize( + env.NORAY_UDP_RELAY_MAX_INDIVIDUAL_TRAFFIC ?? "128kb", + ), + maxGlobalTraffic: byteSize( + env.NORAY_UDP_RELAY_MAX_GLOBAL_TRAFFIC ?? "1gb", + ), + trafficInterval: duration( + env.NORAY_UDP_RELAY_TRAFFIC_INTERVAL ?? "100ms", + ), + maxLifetimeDuration: duration( + env.NORAY_UDP_RELAY_MAX_LIFETIME_DURATION ?? "4hr", + ), + maxLifetimeTraffic: byteSize( + env.NORAY_UDP_RELAY_MAX_LIFETIME_TRAFFIC ?? "4gb", + ), + }, + + loglevel: getLogLevel(), + }; +} + +export function readDefaultConfig() { + return readConfig({}); +} + +export function readLiveConfig() { + dotenv.config(); + return readConfig(process.env); +} + +export type NorayConfig = ReturnType; +export type OidConfig = NorayConfig["oid"]; +export type WordsOidConfig = NorayConfig["words_oid"]; +export type PidConfig = NorayConfig["pid"]; +export type SocketConfig = NorayConfig["socket"]; +export type HttpConfig = NorayConfig["http"]; +export type UdpRelayConfig = NorayConfig["udpRelay"]; + +dotenv.config(); +export const config = readLiveConfig(); +logger.info({ config }, "Loaded application config"); From f5f2e6393735d94ca7990b2b6e602c2c51ade2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 22:19:05 +0200 Subject: [PATCH 06/22] hosts --- src/hosts/host.commands.mjs | 55 --------------------------------- src/hosts/host.commands.ts | 48 +++++++++++++++++++++++++++++ src/hosts/host.entity.mjs | 57 ----------------------------------- src/hosts/host.entity.ts | 56 ++++++++++++++++++++++++++++++++++ src/hosts/host.mjs | 15 --------- src/hosts/host.repository.mjs | 36 ---------------------- src/hosts/host.repository.ts | 32 ++++++++++++++++++++ src/hosts/host.ts | 14 +++++++++ 8 files changed, 150 insertions(+), 163 deletions(-) delete mode 100644 src/hosts/host.commands.mjs create mode 100644 src/hosts/host.commands.ts delete mode 100644 src/hosts/host.entity.mjs create mode 100644 src/hosts/host.entity.ts delete mode 100644 src/hosts/host.mjs delete mode 100644 src/hosts/host.repository.mjs create mode 100644 src/hosts/host.repository.ts create mode 100644 src/hosts/host.ts diff --git a/src/hosts/host.commands.mjs b/src/hosts/host.commands.mjs deleted file mode 100644 index 7d23af1..0000000 --- a/src/hosts/host.commands.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable */ -import { HostRepository } from './host.repository.mjs' -import { NodeSocketReactor } from '@foxssake/trimsock-node' -/* eslint-enable */ -import { HostEntity } from './host.entity.mjs' -import logger from '../logger.mjs' -import * as prometheus from 'prom-client' -import { metricsRegistry } from '../metrics/metrics.registry.mjs' - -const activeHostsGauge = new prometheus.Gauge({ - name: 'noray_active_hosts', - help: 'Number of currently active hosts', - registers: [metricsRegistry] -}) - -/** -* @param {HostRepository} hostRepository -*/ -export function handleRegisterHost (hostRepository) { - /** - * @param {NodeSocketReactor} server - */ - return function (server) { - server.on('register-host', (__, exchange) => { - const log = logger.child({ name: 'cmd:register-host' }) - activeHostsGauge.inc() - - const socket = exchange.source - const host = new HostEntity({ socket }) - hostRepository.add(host) - - exchange.send({ name: 'set-oid', params: [host.oid] }) - exchange.send({ name: 'set-pid', params: [host.pid] }) - - log.info( - { oid: host.oid, pid: host.pid }, - 'Registered host from address %s:%d', - socket.remoteAddress, socket.remotePort - ) - - socket.on('error', err => { - log.error(err) - }) - - socket.on('close', () => { - log.info( - { oid: host.oid, pid: host.pid }, - 'Host disconnected, removing from repository' - ) - hostRepository.removeItem(host) - activeHostsGauge.dec() - }) - }) - } -} diff --git a/src/hosts/host.commands.ts b/src/hosts/host.commands.ts new file mode 100644 index 0000000..d6c780a --- /dev/null +++ b/src/hosts/host.commands.ts @@ -0,0 +1,48 @@ +import { HostRepository } from "./host.repository"; +import { NodeSocketReactor } from "@foxssake/trimsock-node"; +import { makeHost } from "./host.entity"; +import logger from "../logger"; +import * as prometheus from "prom-client"; +import { metricsRegistry } from "../metrics/metrics.registry.mjs"; + +const activeHostsGauge = new prometheus.Gauge({ + name: "noray_active_hosts", + help: "Number of currently active hosts", + registers: [metricsRegistry], +}); + +export function handleRegisterHost(hostRepository: HostRepository) { + return function(server: NodeSocketReactor) { + server.on("register-host", (__, exchange) => { + const log = logger.child({ name: "cmd:register-host" }); + activeHostsGauge.inc(); + + const socket = exchange.source; + const host = makeHost(socket); + hostRepository.add(host); + + exchange.send({ name: "set-oid", params: [host.oid] }); + exchange.send({ name: "set-pid", params: [host.pid] }); + + log.info( + { oid: host.oid, pid: host.pid }, + "Registered host from address %s:%d", + socket.remoteAddress, + socket.remotePort, + ); + + socket.on("error", (err) => { + log.error(err); + }); + + socket.on("close", () => { + log.info( + { oid: host.oid, pid: host.pid }, + "Host disconnected, removing from repository", + ); + hostRepository.removeItem(host); + activeHostsGauge.dec(); + }); + }); + }; +} diff --git a/src/hosts/host.entity.mjs b/src/hosts/host.entity.mjs deleted file mode 100644 index ac9c97a..0000000 --- a/src/hosts/host.entity.mjs +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable */ -import * as net from 'node:net' -import * as dgram from 'node:dgram' -/* eslint-enable */ -import * as nanoid from 'nanoid' -import { config } from '../config.mjs' -import { generateWordId } from '../utils.mjs' - -const generateOID = config.words_oid.enabled ? () => generateWordId(config.words_oid.length) : nanoid.customAlphabet(config.oid.charset, config.oid.length) -const generatePID = nanoid.customAlphabet(config.pid.charset, config.pid.length) - -/** -* Host entity. -* -* Hosts register in advance for other players to connect to them. -*/ -export class HostEntity { - /** - * Open id. - * @type {string} - */ - oid - - /** - * Private id. - * @type {string} - */ - pid - - /** - * Socket. - * @type {net.Socket} - */ - socket - - /** - * Relay port. - * @type {number} - */ - relay - - /** - * Host remote info. - * @type {dgram.RemoteInfo} - */ - rinfo - - /** - * Construct entity. - * @param {HostEntity} options Options - */ - constructor (options) { - options && Object.assign(this, options) - this.oid ??= generateOID() - this.pid ??= generatePID() - } -} diff --git a/src/hosts/host.entity.ts b/src/hosts/host.entity.ts new file mode 100644 index 0000000..ef57552 --- /dev/null +++ b/src/hosts/host.entity.ts @@ -0,0 +1,56 @@ +import * as net from "node:net"; +import * as dgram from "node:dgram"; +import * as nanoid from "nanoid"; +import { config } from "../config"; +import { generateWordId } from "../utils"; + +const generateOID = config.words_oid.enabled + ? () => generateWordId(config.words_oid.length) + : nanoid.customAlphabet(config.oid.charset, config.oid.length); +const generatePID = nanoid.customAlphabet( + config.pid.charset, + config.pid.length, +); + +/** + * Host entity. + * + * Hosts register in advance for other players to connect to them. + */ +export interface HostEntity { + /** + * Open id. + */ + oid: string; + + /** + * Private id. + */ + pid: string; + + /** + * Socket. + */ + socket: net.Socket; + + /** + * Relay port. + */ + relay: number | undefined; + + /** + * Host remote info. + */ + rinfo: dgram.RemoteInfo | undefined; +} + +export function makeHost(socket: net.Socket): HostEntity { + return { + socket, + oid: generateOID(), + pid: generatePID(), + + relay: undefined, + rinfo: undefined, + }; +} diff --git a/src/hosts/host.mjs b/src/hosts/host.mjs deleted file mode 100644 index 05ca56a..0000000 --- a/src/hosts/host.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { Noray } from '../noray.mjs' -import logger from '../logger.mjs' -import { handleRegisterHost } from './host.commands.mjs' -import { HostRepository } from './host.repository.mjs' - -const log = logger.child({ name: 'mod:host' }) - -export const hostRepository = new HostRepository() - -Noray.hook(noray => { - log.info('Registering host commands') - - noray.reactor - .configure(handleRegisterHost(hostRepository)) -}) diff --git a/src/hosts/host.repository.mjs b/src/hosts/host.repository.mjs deleted file mode 100644 index 71d00a5..0000000 --- a/src/hosts/host.repository.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable */ -import * as net from 'node:net' -import { HostEntity } from './host.entity.mjs' -/* eslint-enable */ -import { Repository, fieldIdMapper } from '../repository.mjs' - -/** -* Repository for tracking hosts. -* -* @extends {Repository} -*/ -export class HostRepository extends Repository { - constructor () { - super({ - idMapper: fieldIdMapper('oid') - }) - } - - /** - * Find host by private id. - * @param {string} pid Private id - * @returns {HostEntity|undefined} Host - */ - findByPid (pid) { - return [...this.list()].find(host => host.pid === pid) - } - - /** - * Find host by socket. - * @param {net.Socket} socket Socket - * @returns {HostEntity|undefined} Host - */ - findBySocket (socket) { - return [...this.list()].find(host => host.socket === socket) - } -} diff --git a/src/hosts/host.repository.ts b/src/hosts/host.repository.ts new file mode 100644 index 0000000..7884af3 --- /dev/null +++ b/src/hosts/host.repository.ts @@ -0,0 +1,32 @@ +import * as net from "node:net"; +import { HostEntity } from "./host.entity"; +import { Repository } from "../repository"; + +/** + * Repository for tracking hosts. + * + * @extends {Repository} + */ +export class HostRepository extends Repository { + constructor() { + super((host) => host.oid); + } + + /** + * Find host by private id. + */ + findByPid(pid: string): HostEntity | undefined { + // TODO: Cache by PID + return [...this.list()].find((host) => host.pid === pid); + } + + /** + * Find host by socket. + * @param {net.Socket} socket Socket + * @returns {HostEntity|undefined} Host + */ + findBySocket(socket: net.Socket): HostEntity | undefined { + // TODO: Cache by socket + return [...this.list()].find((host) => host.socket === socket); + } +} diff --git a/src/hosts/host.ts b/src/hosts/host.ts new file mode 100644 index 0000000..bb08f85 --- /dev/null +++ b/src/hosts/host.ts @@ -0,0 +1,14 @@ +import { Noray } from "../noray.mjs"; +import logger from "../logger"; +import { handleRegisterHost } from "./host.commands"; +import { HostRepository } from "./host.repository"; + +const log = logger.child({ name: "mod:host" }); + +export const hostRepository = new HostRepository(); + +Noray.hook((noray: Noray) => { + log.info("Registering host commands"); + + noray.reactor.configure(handleRegisterHost(hostRepository)); +}); From 24b839c96c630a66a24181851f2946d4b03b09d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 22:34:06 +0200 Subject: [PATCH 07/22] more ts --- src/connection/connection.commands.mjs | 105 -------- src/connection/connection.commands.ts | 109 ++++++++ src/connection/connection.mjs | 14 - src/connection/connection.ts | 14 + src/hosts/host.commands.mjs | 2 + src/hosts/host.commands.ts | 2 +- src/hosts/host.ts | 2 +- src/metrics/metrics.mjs | 39 --- src/metrics/metrics.registry.mjs | 3 - src/metrics/metrics.registry.ts | 3 + src/metrics/metrics.ts | 43 +++ src/noray.mjs | 99 ------- src/noray.ts | 96 +++++++ src/relay/relay.mjs | 126 +++++---- src/relay/udp.relay.cleanup.mjs | 36 +-- src/relay/udp.relay.handler.mjs | 347 +++++++++++++------------ src/relay/udp.remote.registrar.mjs | 152 +++++------ src/repository.ts | 2 +- 18 files changed, 608 insertions(+), 586 deletions(-) delete mode 100644 src/connection/connection.commands.mjs create mode 100644 src/connection/connection.commands.ts delete mode 100644 src/connection/connection.mjs create mode 100644 src/connection/connection.ts create mode 100644 src/hosts/host.commands.mjs delete mode 100644 src/metrics/metrics.mjs delete mode 100644 src/metrics/metrics.registry.mjs create mode 100644 src/metrics/metrics.registry.ts create mode 100644 src/metrics/metrics.ts delete mode 100644 src/noray.mjs create mode 100644 src/noray.ts diff --git a/src/connection/connection.commands.mjs b/src/connection/connection.commands.mjs deleted file mode 100644 index 9799c29..0000000 --- a/src/connection/connection.commands.mjs +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable */ -import { HostRepository } from '../hosts/host.repository.mjs' -import { NodeSocketReactor } from '@foxssake/trimsock-node' -/* eslint-enable */ -import assert from 'node:assert' -import logger from '../logger.mjs' -import { udpRelayHandler } from '../relay/relay.mjs' -import { RelayEntry } from '../relay/relay.entry.mjs' -import { NetAddress } from '../relay/net.address.mjs' - -/** -* @param {HostRepository} hostRepository -*/ -export function handleConnect (hostRepository) { - /** - * @param {NodeSocketReactor} server - */ - return function (server) { - server.on('connect', (command, exchange) => { - const log = logger.child({ name: 'cmd:connect' }) - - const socket = exchange.source - const oid = command.requireText() - const host = hostRepository.find(oid) - const client = hostRepository.findBySocket(socket) - - log.debug( - { oid, client: socket.address() }, - 'Client attempting to connect to host' - ) - - assert(host, 'Unknown host oid: ' + oid) - assert(host.rinfo, 'Host has no remote info registered!') - assert(client, 'Unknown client from address') - assert(client.rinfo, 'Client has no remote info registered!') - - const hostAddress = stringifyAddress(host.rinfo) - const clientAddress = stringifyAddress(client.rinfo) - - server.send(socket, { name: 'connect', params: [hostAddress] }) - server.send(host.socket, { name: 'connect', params: [clientAddress] }) - - log.debug( - { client: clientAddress, host: hostAddress, oid }, - 'Connected client to host' - ) - }) - } -} - -/** -* @param {HostRepository} hostRepository -*/ -export function handleConnectRelay (hostRepository) { - /** - * @param {NodeSocketReactor} server - */ - return function (server) { - server.on('connect-relay', (command, exchange) => { - const log = logger.child({ name: 'cmd:connect-relay' }) - - const socket = exchange.source - const oid = command.requireText() - const host = hostRepository.find(oid) - const client = hostRepository.findBySocket(socket) - - log.debug( - { oid, client: `${socket.remoteAddress}:${socket.remotePort}` }, - 'Client attempting to connect to host' - ) - assert(host, 'Unknown host oid: ' + oid) - assert(client, 'Unknown client from address') - - log.debug('Ensuring relay for both parties') - host.relay = getRelay(host.rinfo) - client.relay = getRelay(client.rinfo) - - log.debug({ host: host.relay, client: client.relay }, 'Replying with relay') - server.send(socket, { name: 'connect-relay', params: [host.relay.toString()] }) - server.send(host.socket, { name: 'connect-relay', params: [client.relay.toString()] }) - log.debug( - { client: `${socket.remoteAddress}:${socket.remotePort}`, relay: host.relay, oid }, - 'Connected client to host' - ) - }) - } -} - -function stringifyAddress (address) { - return `${address.address}:${address.port}` -} - -function getRelay (rinfo) { - // Attempt to create new relay on each connect - // If there's a relay already, UDPRelayHandler will return that - // If there's no relay, or it has expired, a new one will be created - const log = logger.child({ name: 'getRelay' }) - log.trace({ rinfo }, 'Ensuring relay for remote') - const relayEntry = udpRelayHandler.createRelay( - new RelayEntry({ address: NetAddress.fromRinfo(rinfo) }) - ) - - log.trace({ relayEntry }, 'Created relay, returning with port %d', relayEntry.port) - return relayEntry.port -} diff --git a/src/connection/connection.commands.ts b/src/connection/connection.commands.ts new file mode 100644 index 0000000..86dbb45 --- /dev/null +++ b/src/connection/connection.commands.ts @@ -0,0 +1,109 @@ +import { HostRepository } from "../hosts/host.repository.js"; +import { NodeSocketReactor } from "@foxssake/trimsock-node"; +import { RemoteInfo } from "node:dgram"; +import assert from "node:assert"; +import logger from "../logger"; +import { udpRelayHandler } from "../relay/relay.mjs"; +import { RelayEntry } from "../relay/relay.entry.mjs"; +import { NetAddress } from "../relay/net.address.mjs"; + +export function handleConnect(hostRepository: HostRepository) { + return function(server: NodeSocketReactor) { + server.on("connect", (command, exchange) => { + const log = logger.child({ name: "cmd:connect" }); + + const socket = exchange.source; + const oid = command.requireText(); + const host = hostRepository.find(oid); + const client = hostRepository.findBySocket(socket); + + log.debug( + { oid, client: socket.address() }, + "Client attempting to connect to host", + ); + + assert(host, "Unknown host oid: " + oid); + assert(host.rinfo, "Host has no remote info registered!"); + assert(client, "Unknown client from address"); + assert(client.rinfo, "Client has no remote info registered!"); + + const hostAddress = stringifyAddress(host.rinfo); + const clientAddress = stringifyAddress(client.rinfo); + + server.send(socket, { name: "connect", params: [hostAddress] }); + server.send(host.socket, { name: "connect", params: [clientAddress] }); + + log.debug( + { client: clientAddress, host: hostAddress, oid }, + "Connected client to host", + ); + }); + }; +} + +export function handleConnectRelay(hostRepository: HostRepository) { + return function(server: NodeSocketReactor) { + server.on("connect-relay", (command, exchange) => { + const log = logger.child({ name: "cmd:connect-relay" }); + + const socket = exchange.source; + const oid = command.requireText(); + const host = hostRepository.find(oid); + const client = hostRepository.findBySocket(socket); + + log.debug( + { oid, client: `${socket.remoteAddress}:${socket.remotePort}` }, + "Client attempting to connect to host", + ); + assert(host, "Unknown host oid: " + oid); + assert(client, "Unknown client from address"); + + log.debug("Ensuring relay for both parties"); + host.relay = getRelay(host.rinfo!!); + client.relay = getRelay(client.rinfo!!); + + log.debug( + { host: host.relay, client: client.relay }, + "Replying with relay", + ); + server.send(socket, { + name: "connect-relay", + params: [host.relay!!.toString()], + }); + server.send(host.socket, { + name: "connect-relay", + params: [client.relay!!.toString()], + }); + log.debug( + { + client: `${socket.remoteAddress}:${socket.remotePort}`, + relay: host.relay, + oid, + }, + "Connected client to host", + ); + }); + }; +} + +function stringifyAddress(address: RemoteInfo) { + return `${address.address}:${address.port}`; +} + +function getRelay(rinfo: RemoteInfo) { + // Attempt to create new relay on each connect + // If there's a relay already, UDPRelayHandler will return that + // If there's no relay, or it has expired, a new one will be created + const log = logger.child({ name: "getRelay" }); + log.trace({ rinfo }, "Ensuring relay for remote"); + const relayEntry = udpRelayHandler.createRelay( + new RelayEntry({ address: NetAddress.fromRinfo(rinfo) }), + ); + + log.trace( + { relayEntry }, + "Created relay, returning with port %d", + relayEntry.port, + ); + return relayEntry.port; +} diff --git a/src/connection/connection.mjs b/src/connection/connection.mjs deleted file mode 100644 index dc0b6b7..0000000 --- a/src/connection/connection.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { Noray } from '../noray.mjs' -import logger from '../logger.mjs' -import { handleConnect, handleConnectRelay } from './connection.commands.mjs' -import { hostRepository } from '../hosts/host.mjs' - -const log = logger.child({ name: 'mod:connection' }) - -Noray.hook(noray => { - log.info('Registering connection commands') - - noray.reactor - .configure(handleConnect(hostRepository)) - .configure(handleConnectRelay(hostRepository)) -}) diff --git a/src/connection/connection.ts b/src/connection/connection.ts new file mode 100644 index 0000000..8a21481 --- /dev/null +++ b/src/connection/connection.ts @@ -0,0 +1,14 @@ +import { Noray } from "../noray.js"; +import logger from "../logger"; +import { handleConnect, handleConnectRelay } from "./connection.commands"; +import { hostRepository } from "../hosts/host"; + +const log = logger.child({ name: "mod:connection" }); + +Noray.hook((noray: Noray) => { + log.info("Registering connection commands"); + + noray.reactor + .configure(handleConnect(hostRepository)) + .configure(handleConnectRelay(hostRepository)); +}); diff --git a/src/hosts/host.commands.mjs b/src/hosts/host.commands.mjs new file mode 100644 index 0000000..ac685c7 --- /dev/null +++ b/src/hosts/host.commands.mjs @@ -0,0 +1,2 @@ + +../metrics/metrics.registry.js diff --git a/src/hosts/host.commands.ts b/src/hosts/host.commands.ts index d6c780a..46bb12a 100644 --- a/src/hosts/host.commands.ts +++ b/src/hosts/host.commands.ts @@ -3,7 +3,7 @@ import { NodeSocketReactor } from "@foxssake/trimsock-node"; import { makeHost } from "./host.entity"; import logger from "../logger"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry.mjs"; +import { metricsRegistry } from "../metrics/metrics.registry.js"; const activeHostsGauge = new prometheus.Gauge({ name: "noray_active_hosts", diff --git a/src/hosts/host.ts b/src/hosts/host.ts index bb08f85..8224451 100644 --- a/src/hosts/host.ts +++ b/src/hosts/host.ts @@ -1,4 +1,4 @@ -import { Noray } from "../noray.mjs"; +import { Noray } from "../noray.js"; import logger from "../logger"; import { handleRegisterHost } from "./host.commands"; import { HostRepository } from "./host.repository"; diff --git a/src/metrics/metrics.mjs b/src/metrics/metrics.mjs deleted file mode 100644 index 213e36b..0000000 --- a/src/metrics/metrics.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import * as http from 'node:http' -import { Noray } from '../noray.mjs' -import logger from '../logger.mjs' -import * as prometheus from 'prom-client' -import { config } from '../config.mjs' -import { metricsRegistry } from './metrics.registry.mjs' - -const log = logger.child({ name: 'mod:metrics' }) - -Noray.hook(noray => { - log.info('Collecting default metrics') - prometheus.collectDefaultMetrics({ - register: metricsRegistry - }) - - log.info('Starting HTTP server to serve metrics') - - const httpServer = new http.Server() - httpServer.on('request', async (req, res) => { - if (req.url !== '/metrics') { - res.statusCode = 404 - res.end() - return - } - - res.write(await metricsRegistry.metrics()) - res.end() - }) - - httpServer.listen(config.http.port, config.http.host, - () => log.info('Serving metrics over HTTP on port %s:%d', config.http.host, config.http.port) - ) - - noray.on('close', () => { - log.info('noray closing, shutting down HTTP server') - httpServer.close() - httpServer.closeAllConnections() - }) -}) diff --git a/src/metrics/metrics.registry.mjs b/src/metrics/metrics.registry.mjs deleted file mode 100644 index dee25ba..0000000 --- a/src/metrics/metrics.registry.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import * as prometheus from 'prom-client' - -export const metricsRegistry = new prometheus.Registry() diff --git a/src/metrics/metrics.registry.ts b/src/metrics/metrics.registry.ts new file mode 100644 index 0000000..8a8eb81 --- /dev/null +++ b/src/metrics/metrics.registry.ts @@ -0,0 +1,3 @@ +import * as prometheus from "prom-client"; + +export const metricsRegistry = new prometheus.Registry(); diff --git a/src/metrics/metrics.ts b/src/metrics/metrics.ts new file mode 100644 index 0000000..d1b366e --- /dev/null +++ b/src/metrics/metrics.ts @@ -0,0 +1,43 @@ +import * as http from "node:http"; +import { Noray } from "../noray.js"; +import logger from "../logger"; +import * as prometheus from "prom-client"; +import { config } from "../config"; +import { metricsRegistry } from "./metrics.registry"; + +const log = logger.child({ name: "mod:metrics" }); + +Noray.hook((noray: Noray) => { + log.info("Collecting default metrics"); + prometheus.collectDefaultMetrics({ + register: metricsRegistry, + }); + + log.info("Starting HTTP server to serve metrics"); + + const httpServer = new http.Server(); + httpServer.on("request", async (req, res) => { + if (req.url !== "/metrics") { + res.statusCode = 404; + res.end(); + return; + } + + res.write(await metricsRegistry.metrics()); + res.end(); + }); + + httpServer.listen(config.http.port, config.http.host, () => + log.info( + "Serving metrics over HTTP on port %s:%d", + config.http.host, + config.http.port, + ), + ); + + noray.on("close", () => { + log.info("noray closing, shutting down HTTP server"); + httpServer.close(); + httpServer.closeAllConnections(); + }); +}); diff --git a/src/noray.mjs b/src/noray.mjs deleted file mode 100644 index 9fd43d0..0000000 --- a/src/noray.mjs +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable */ -import * as net from 'node:net' -/* eslint-enable */ -import { EventEmitter } from 'node:events' -import logger from './logger.mjs' -import { config } from './config.mjs' -import { NodeSocketReactor } from '@foxssake/trimsock-node' -import { promiseEvent } from './utils.mjs' - -const defaultModules = [ - 'metrics/metrics.mjs', - 'relay/relay.mjs', - 'hosts/host.mjs', - 'connection/connection.mjs' -] - -const hooks = [] - -export class Noray extends EventEmitter { - /** @type {net.Server} */ - #server - - /** @type {NodeSocketReactor} */ - #reactor - - #log = logger - - /** - * Register a Noray configuration hook. - * @param {function(Noray)} h Hook - */ - static hook (h) { - hooks.push(h) - } - - async start (modules) { - modules ??= defaultModules - - this.#log.info('Starting Noray') - - this.#reactor = new NodeSocketReactor() - .onError((command, exchange, error) => { - exchange.failOrSend({ name: command.name, data: '' + error }) - }) - - // Import modules for hooks - for (const m of modules) { - this.#log.info('Pulling module %s for hooks', m) - await import(`../src/${m}`) - } - - // Run hooks - this.#log.info('Running %d hooks', hooks.length) - const hookPromises = hooks.map(h => h(this)) - this.#log.info('Hooks launched') - - this.#log.info('Waiting for hooks to finish') - await Promise.all(hookPromises) - - // Start server - this.#log.info('Starting TCP server') - this.#server = this.#reactor.serve() - this.#server.listen(config.socket.port, config.socket.host) - this.#server.on('listening', () => { - this.#log.info( - 'Listening on %s:%s', - config.socket.host, config.socket.port - ) - - this.#server.on('error', err => { - this.#log.error('Listen socket encountered an error!') - this.#log.error(err) - }) - - this.#server.on('connection', conn => { - conn.on('error', err => { - this.#log.error('Connection socket encountered an error!') - this.#log.error(err) - }) - }) - - this.emit('listening', config.socket.port, config.socket.host) - }) - - await promiseEvent(this, 'listening') - this.#log.info('Started noray in %f ms', process.uptime() * 1000.0) - } - - shutdown () { - this.#log.info('Shutting down') - - this.emit('close') - this.#server.close() - } - - get reactor () { - return this.#reactor - } -} diff --git a/src/noray.ts b/src/noray.ts new file mode 100644 index 0000000..dff782c --- /dev/null +++ b/src/noray.ts @@ -0,0 +1,96 @@ +import * as net from "node:net"; +import { EventEmitter } from "node:events"; +import logger from "./logger"; +import { config } from "./config"; +import { NodeSocketReactor } from "@foxssake/trimsock-node"; +import { promiseEvent } from "./utils"; + +export type NorayHook = (noray: Noray) => void; + +const defaultModules = [ + "metrics/metrics.mjs", + "relay/relay.mjs", + "hosts/host.mjs", + "connection/connection.mjs", +]; + +export class Noray extends EventEmitter { + private server!: net.Server; + private _reactor!: NodeSocketReactor; + private log = logger; + + private static hooks: NorayHook[] = []; + + /** + * Register a Noray configuration hook. + */ + static hook(hok: NorayHook) { + this.hooks.push(hok); + } + + async start(modules: string[]): Promise { + modules ??= defaultModules; + + this.log.info("Starting Noray"); + + this._reactor = new NodeSocketReactor().onError( + (command, exchange, error) => { + exchange.failOrSend({ name: command.name, text: "" + error }); + }, + ); + + // Import modules for hooks + for (const m of modules) { + this.log.info("Pulling module %s for hooks", m); + await import(`../src/${m}`); + } + + // Run hooks + this.log.info("Running %d hooks", Noray.hooks.length); + const hookPromises = Noray.hooks.map((h) => h(this)); + this.log.info("Hooks launched"); + + this.log.info("Waiting for hooks to finish"); + await Promise.all(hookPromises); + + // Start server + this.log.info("Starting TCP server"); + this.server = this.reactor.serve(); + this.server.listen(config.socket.port, config.socket.host); + this.server.on("listening", () => { + this.log.info( + "Listening on %s:%s", + config.socket.host, + config.socket.port, + ); + + this.server.on("error", (err) => { + this.log.error("Listen socket encountered an error!"); + this.log.error(err); + }); + + this.server.on("connection", (conn) => { + conn.on("error", (err) => { + this.log.error("Connection socket encountered an error!"); + this.log.error(err); + }); + }); + + this.emit("listening", config.socket.port, config.socket.host); + }); + + await promiseEvent(this, "listening"); + this.log.info("Started noray in %f ms", process.uptime() * 1000.0); + } + + shutdown() { + this.log.info("Shutting down"); + + this.emit("close"); + this.server.close(); + } + + get reactor() { + return this._reactor; + } +} diff --git a/src/relay/relay.mjs b/src/relay/relay.mjs index bfa0e81..1a71b51 100644 --- a/src/relay/relay.mjs +++ b/src/relay/relay.mjs @@ -1,86 +1,100 @@ -import { config } from '../config.mjs' -import { constrainGlobalBandwidth, constrainIndividualBandwidth, constrainLifetime, constrainTraffic } from './constraints.mjs' -import { UDPRelayHandler } from './udp.relay.handler.mjs' -import { Noray } from '../noray.mjs' -import { cleanupUdpRelayTable } from './udp.relay.cleanup.mjs' -import logger from '../logger.mjs' -import { formatByteSize, formatDuration } from '../utils.mjs' -import { UDPRemoteRegistrar } from './udp.remote.registrar.mjs' -import { hostRepository } from '../hosts/host.mjs' -import { useDynamicRelay } from './dynamic.relaying.mjs' -import { UDPSocketPool } from './udp.socket.pool.mjs' - -export const udpSocketPool = new UDPSocketPool() - -export const udpRelayHandler = new UDPRelayHandler({ socketPool: udpSocketPool }) +import { config } from "../config.mjs"; +import { + constrainGlobalBandwidth, + constrainIndividualBandwidth, + constrainLifetime, + constrainTraffic, +} from "./constraints.mjs"; +import { UDPRelayHandler } from "./udp.relay.handler.mjs"; +import { Noray } from "../noray.js"; +import { cleanupUdpRelayTable } from "./udp.relay.cleanup.mjs"; +import logger from "../logger.mjs"; +import { formatByteSize, formatDuration } from "../utils.mjs"; +import { UDPRemoteRegistrar } from "./udp.remote.registrar.mjs"; +import { hostRepository } from "../hosts/host.js"; +import { useDynamicRelay } from "./dynamic.relaying.mjs"; +import { UDPSocketPool } from "./udp.socket.pool.mjs"; + +export const udpSocketPool = new UDPSocketPool(); + +export const udpRelayHandler = new UDPRelayHandler({ + socketPool: udpSocketPool, +}); export const udpRemoteRegistrar = new UDPRemoteRegistrar({ hostRepository, - udpRelayHandler -}) -const log = logger.child({ name: 'mod:relay' }) + udpRelayHandler, +}); +const log = logger.child({ name: "mod:relay" }); -Noray.hook(async noray => { +Noray.hook(async (noray) => { log.info( - 'Starting periodic UDP relay cleanup job, running every %s', - formatDuration(config.udpRelay.cleanupInterval) - ) + "Starting periodic UDP relay cleanup job, running every %s", + formatDuration(config.udpRelay.cleanupInterval), + ); const cleanupJob = setInterval( () => cleanupUdpRelayTable(udpRelayHandler, config.udpRelay.timeout), - config.udpRelay.cleanupInterval * 1000 - ) + config.udpRelay.cleanupInterval * 1000, + ); - log.info('Listening on port %d for UDP remote registrars', config.udpRelay.registrarPort) - udpRemoteRegistrar.listen(config.udpRelay.registrarPort) + log.info( + "Listening on port %d for UDP remote registrars", + config.udpRelay.registrarPort, + ); + udpRemoteRegistrar.listen(config.udpRelay.registrarPort); - log.info('Binding %d ports for relaying', config.udpRelay.ports.length) + log.info("Binding %d ports for relaying", config.udpRelay.ports.length); for (const port of config.udpRelay.ports) { - log.trace('Binding port %d for relay', port) + log.trace("Binding port %d for relay", port); try { - await udpSocketPool.allocatePort(port) + await udpSocketPool.allocatePort(port); } catch (err) { - log.warn({ err }, 'Failed to bind port %d, ignoring', port) + log.warn({ err }, "Failed to bind port %d, ignoring", port); } } log.info( - 'Limiting relay bandwidth to %s/s and global bandwidth to %s/s', + "Limiting relay bandwidth to %s/s and global bandwidth to %s/s", formatByteSize(config.udpRelay.maxIndividualTraffic), - formatByteSize(config.udpRelay.maxGlobalTraffic) - ) + formatByteSize(config.udpRelay.maxGlobalTraffic), + ); constrainIndividualBandwidth( - udpRelayHandler, config.udpRelay.maxIndividualTraffic, config.udpRelay.trafficInterval - ) + udpRelayHandler, + config.udpRelay.maxIndividualTraffic, + config.udpRelay.trafficInterval, + ); constrainGlobalBandwidth( - udpRelayHandler, config.udpRelay.maxGlobalTraffic, config.udpRelay.trafficInterval - ) + udpRelayHandler, + config.udpRelay.maxGlobalTraffic, + config.udpRelay.trafficInterval, + ); log.info( - 'Blocking relay traffic after %s or %s', + "Blocking relay traffic after %s or %s", formatDuration(config.udpRelay.maxLifetimeDuration), - formatByteSize(config.udpRelay.maxLifetimeTraffic) - ) + formatByteSize(config.udpRelay.maxLifetimeTraffic), + ); - constrainLifetime(udpRelayHandler, config.udpRelay.maxLifetimeDuration) - constrainTraffic(udpRelayHandler, config.udpRelay.maxLifetimeTraffic) + constrainLifetime(udpRelayHandler, config.udpRelay.maxLifetimeDuration); + constrainTraffic(udpRelayHandler, config.udpRelay.maxLifetimeTraffic); - log.info('Applying dynamic relaying') - useDynamicRelay(udpRelayHandler) + log.info("Applying dynamic relaying"); + useDynamicRelay(udpRelayHandler); - log.info('Adding shutdown hooks') - noray.on('close', () => { - log.info('Noray shutting down, cancelling UDP relay cleanup job') - clearInterval(cleanupJob) + log.info("Adding shutdown hooks"); + noray.on("close", () => { + log.info("Noray shutting down, cancelling UDP relay cleanup job"); + clearInterval(cleanupJob); - log.info('Closing UDP remote registrar socket') - udpRemoteRegistrar.socket.close() + log.info("Closing UDP remote registrar socket"); + udpRemoteRegistrar.socket.close(); - log.info('Closing socket pool') - udpSocketPool.clear() + log.info("Closing socket pool"); + udpSocketPool.clear(); - log.info('Closing relay handler') - udpRelayHandler.clear() - }) -}) + log.info("Closing relay handler"); + udpRelayHandler.clear(); + }); +}); diff --git a/src/relay/udp.relay.cleanup.mjs b/src/relay/udp.relay.cleanup.mjs index a4c1d54..bdf757c 100644 --- a/src/relay/udp.relay.cleanup.mjs +++ b/src/relay/udp.relay.cleanup.mjs @@ -1,29 +1,29 @@ /* eslint-disable */ -import { UDPRelayHandler } from './udp.relay.handler.mjs' +import { UDPRelayHandler } from "./udp.relay.handler.mjs"; /* eslint-enable */ -import { time } from '../utils.mjs' -import * as prometheus from 'prom-client' -import { metricsRegistry } from '../metrics/metrics.registry.mjs' +import { time } from "../utils.mjs"; +import * as prometheus from "prom-client"; +import { metricsRegistry } from "../metrics/metrics.registry.js"; const expiredRelayCounter = new prometheus.Counter({ - name: 'noray_relay_expired', - help: 'Count of expired relays', - registers: [metricsRegistry] -}) + name: "noray_relay_expired", + help: "Count of expired relays", + registers: [metricsRegistry], +}); /** -* Remove idle relays. -* @param {UDPRelayHandler} relayHandler Relay handler -* @param {number} timeout Maximum relay age in seconds -*/ -export function cleanupUdpRelayTable (relayHandler, timeout) { - const timeCutoff = time() - timeout + * Remove idle relays. + * @param {UDPRelayHandler} relayHandler Relay handler + * @param {number} timeout Maximum relay age in seconds + */ +export function cleanupUdpRelayTable(relayHandler, timeout) { + const timeCutoff = time() - timeout; relayHandler.relayTable - .map(relay => [relay, Math.max(relay.lastSent, relay.lastReceived)]) + .map((relay) => [relay, Math.max(relay.lastSent, relay.lastReceived)]) .filter(([_, lastTraffic]) => lastTraffic <= timeCutoff) .forEach(([relay, _]) => { - relayHandler.freeRelay(relay) - expiredRelayCounter.inc() - }) + relayHandler.freeRelay(relay); + expiredRelayCounter.inc(); + }); } diff --git a/src/relay/udp.relay.handler.mjs b/src/relay/udp.relay.handler.mjs index 8dba129..23d5dad 100644 --- a/src/relay/udp.relay.handler.mjs +++ b/src/relay/udp.relay.handler.mjs @@ -1,254 +1,255 @@ /* eslint-disable */ -import { RelayEntry } from './relay.entry.mjs' +import { RelayEntry } from "./relay.entry.mjs"; /* eslint-enable */ -import { NetAddress } from './net.address.mjs' -import { UDPSocketPool } from './udp.socket.pool.mjs' -import { time } from '../utils.mjs' -import { EventEmitter } from 'node:events' -import logger from '../logger.mjs' -import * as prometheus from 'prom-client' -import { metricsRegistry } from '../metrics/metrics.registry.mjs' +import { NetAddress } from "./net.address.mjs"; +import { UDPSocketPool } from "./udp.socket.pool.mjs"; +import { time } from "../utils.mjs"; +import { EventEmitter } from "node:events"; +import logger from "../logger.mjs"; +import * as prometheus from "prom-client"; +import { metricsRegistry } from "../metrics/metrics.registry.js"; -const log = logger.child({ name: 'UDPRelayHandler' }) +const log = logger.child({ name: "UDPRelayHandler" }); const relayDurationHistogram = new prometheus.Histogram({ - name: 'noray_relay_duration', - help: 'Time it takes to relay a packet', - registers: [metricsRegistry] -}) + name: "noray_relay_duration", + help: "Time it takes to relay a packet", + registers: [metricsRegistry], +}); const relaySizeHistorgram = new prometheus.Histogram({ - name: 'noray_relay_size', - help: 'Size of the packet being relayed', - registers: [metricsRegistry] -}) + name: "noray_relay_size", + help: "Size of the packet being relayed", + registers: [metricsRegistry], +}); const relayDropCounter = new prometheus.Counter({ - name: 'noray_relay_drop_count', - help: 'Number of relay packets dropped', - registers: [metricsRegistry] -}) + name: "noray_relay_drop_count", + help: "Number of relay packets dropped", + registers: [metricsRegistry], +}); const activeRelayGauge = new prometheus.Gauge({ - name: 'noray_relay_count', - help: 'Count of currently active relays', - registers: [metricsRegistry] -}) + name: "noray_relay_count", + help: "Count of currently active relays", + registers: [metricsRegistry], +}); /** -* Class implementing the actual relay logic. -* -* The relay handler keeps an internal table of relay entries and a socket pool. -* -* Whenever a new relay is added, the socket pool ensures that we have the -* necessary local port allocated to listen for incoming traffic on that port. -* -* When traffic arrives on any of the listening ports, it is first checked in -* the translation table. If there's an entry both for the sender AND target, -* the traffic is forwarded as-is, through the port dedicated to the sender. -* -* Example: Port 1 is allocated for Host, port 2 is allocated for Client. When -* we get a packet targeting port 1 from Client, we use port 2 to relay the data -* to Host. This way, Client will always appear as Noray:2 to the Host. -*/ + * Class implementing the actual relay logic. + * + * The relay handler keeps an internal table of relay entries and a socket pool. + * + * Whenever a new relay is added, the socket pool ensures that we have the + * necessary local port allocated to listen for incoming traffic on that port. + * + * When traffic arrives on any of the listening ports, it is first checked in + * the translation table. If there's an entry both for the sender AND target, + * the traffic is forwarded as-is, through the port dedicated to the sender. + * + * Example: Port 1 is allocated for Host, port 2 is allocated for Client. When + * we get a packet targeting port 1 from Client, we use port 2 to relay the data + * to Host. This way, Client will always appear as Noray:2 to the Host. + */ export class UDPRelayHandler extends EventEmitter { /** @type {UDPSocketPool} */ - #socketPool + #socketPool; /** @type {RelayEntry[]} */ - #relayTable = [] + #relayTable = []; /** - * Construct instance. - * @param {object} options Options - * @param {UDPSocketPool} [options.socketPool] Socket pool - */ - constructor (options) { - super() - - this.#socketPool = options?.socketPool ?? new UDPSocketPool() + * Construct instance. + * @param {object} options Options + * @param {UDPSocketPool} [options.socketPool] Socket pool + */ + constructor(options) { + super(); + + this.#socketPool = options?.socketPool ?? new UDPSocketPool(); } /** - * Create a relay entry. - * - * If there's already a relay for the address, returns that. - * NOTE: This modifies the incoming relay and returns the same instance. - * @param {RelayEntry} relay Relay - * @return {Promise} Resulting relay - * @fires UDPRelayHandler#create - */ - createRelay (relay) { - log.debug({ relay }, 'Creating relay') + * Create a relay entry. + * + * If there's already a relay for the address, returns that. + * NOTE: This modifies the incoming relay and returns the same instance. + * + * @param {RelayEntry} relay Relay + * @return {RelayEntry} Resulting relay + * @fires UDPRelayHandler#create + */ + createRelay(relay) { + log.debug({ relay }, "Creating relay"); if (this.hasRelay(relay)) { // We already have this relay entry - log.trace({ relay }, 'Relay already exists, ignoring') - return this.#relayTable.find(e => e.equals(relay)) + log.trace({ relay }, "Relay already exists, ignoring"); + return this.#relayTable.find((e) => e.equals(relay)); } - relay.port = this.#socketPool.getPort() - this.emit('create', relay) + relay.port = this.#socketPool.getPort(); + this.emit("create", relay); - const socket = this.#socketPool.getSocket(relay.port) - socket.removeAllListeners('message') - .on('message', (msg, rinfo) => { - this.relay(msg, NetAddress.fromRinfo(rinfo), relay.port) - }) + const socket = this.#socketPool.getSocket(relay.port); + socket.removeAllListeners("message").on("message", (msg, rinfo) => { + this.relay(msg, NetAddress.fromRinfo(rinfo), relay.port); + }); - relay.lastReceived = time() - relay.created = time() - this.#relayTable.push(relay) - log.trace({ relay }, 'Relay created') + relay.lastReceived = time(); + relay.created = time(); + this.#relayTable.push(relay); + log.trace({ relay }, "Relay created"); - activeRelayGauge.inc() + activeRelayGauge.inc(); - return relay + return relay; } /** - * Check if relay already exists in the table. - * - * NOTE: This only compares the addresses, not the allocated port. - * @param {RelayEntry} relay Relay - * @returns {boolean} True if relay already exists - */ - hasRelay (relay) { - return this.#relayTable.find(e => e.equals(relay)) !== undefined + * Check if relay already exists in the table. + * + * NOTE: This only compares the addresses, not the allocated port. + * @param {RelayEntry} relay Relay + * @returns {boolean} True if relay already exists + */ + hasRelay(relay) { + return this.#relayTable.find((e) => e.equals(relay)) !== undefined; } /** - * Free a relay entry, removing it from the table and freeing any associated resources. - * @param {RelayEntry} relay Relay - * @returns {boolean} True if a relay was freed - * @fires UDPRelayHandler#destroy - */ - freeRelay (relay) { - const idx = this.#relayTable.findIndex(e => e.equals(relay)) + * Free a relay entry, removing it from the table and freeing any associated resources. + * @param {RelayEntry} relay Relay + * @returns {boolean} True if a relay was freed + * @fires UDPRelayHandler#destroy + */ + freeRelay(relay) { + const idx = this.#relayTable.findIndex((e) => e.equals(relay)); if (idx < 0) { - return false + return false; } - this.emit('destroy', relay) + this.emit("destroy", relay); - this.#socketPool.returnPort(relay.port) - this.#relayTable = this.#relayTable.filter((_, i) => i !== idx) + this.#socketPool.returnPort(relay.port); + this.#relayTable = this.#relayTable.filter((_, i) => i !== idx); - activeRelayGauge.dec() + activeRelayGauge.dec(); - return true + return true; } /** - * Free all relay entries. - */ - clear () { - this.relayTable.forEach(entry => this.freeRelay(entry)) + * Free all relay entries. + */ + clear() { + this.relayTable.forEach((entry) => this.freeRelay(entry)); - activeRelayGauge.reset() + activeRelayGauge.reset(); } /** - * Relay a message from a given sender to target. - * @param {Buffer} msg Message - * @param {NetAddress} sender Sender address - * @param {number} target Target port - * @returns {Promise} True on success - * @fires UDPRelayHandler#transmit - * @fires UDPRelayHandler#drop - */ - relay (msg, sender, target) { - const measure = relayDurationHistogram.startTimer() - - const senderRelay = this.#relayTable.find(r => - r.address.port === sender.port && r.address.address === sender.address - ) - const targetRelay = this.#relayTable.find(r => r.port === target) + * Relay a message from a given sender to target. + * @param {Buffer} msg Message + * @param {NetAddress} sender Sender address + * @param {number} target Target port + * @returns {Promise} True on success + * @fires UDPRelayHandler#transmit + * @fires UDPRelayHandler#drop + */ + relay(msg, sender, target) { + const measure = relayDurationHistogram.startTimer(); + + const senderRelay = this.#relayTable.find( + (r) => + r.address.port === sender.port && r.address.address === sender.address, + ); + const targetRelay = this.#relayTable.find((r) => r.port === target); if (!senderRelay || !targetRelay) { // We don't have a relay for the sender, target, or both - this.emit('drop', senderRelay, targetRelay, sender, target, msg) + this.emit("drop", senderRelay, targetRelay, sender, target, msg); - relayDropCounter.inc() - measure() + relayDropCounter.inc(); + measure(); - return false + return false; } - const socket = this.#socketPool.getSocket(senderRelay.port) + const socket = this.#socketPool.getSocket(senderRelay.port); if (!socket) { // For some reason we don't have the socket - return false + return false; } - this.emit('transmit', senderRelay, targetRelay, msg) + this.emit("transmit", senderRelay, targetRelay, msg); - socket.send(msg, targetRelay.address.port, targetRelay.address.address) + socket.send(msg, targetRelay.address.port, targetRelay.address.address); // Keep track of traffic timings - senderRelay.lastReceived = time() - targetRelay.lastSent = time() + senderRelay.lastReceived = time(); + targetRelay.lastSent = time(); - relaySizeHistorgram.observe(msg?.byteLength ?? 0) - measure() + relaySizeHistorgram.observe(msg?.byteLength ?? 0); + measure(); - return true + return true; } /** - * Socket pool used for relays. - * @type {UDPSocketPool} - */ - get socketPool () { - return this.#socketPool + * Socket pool used for relays. + * @type {UDPSocketPool} + */ + get socketPool() { + return this.#socketPool; } /** - * Relay table used for relays. - * @type {RelayEntry[]} - */ - get relayTable () { - return [...this.#relayTable] + * Relay table used for relays. + * @type {RelayEntry[]} + */ + get relayTable() { + return [...this.#relayTable]; } } /** -* Relay creation event. -* -* This is emitted *before* the relay is pushed, giving the handler a change to -* reject by throwing. -* @event UDPRelayHandler#create -* @param {RelayEntry} relay Relay entry -*/ + * Relay creation event. + * + * This is emitted *before* the relay is pushed, giving the handler a change to + * reject by throwing. + * @event UDPRelayHandler#create + * @param {RelayEntry} relay Relay entry + */ /** -* Relay transmission event. -* -* This event is emitted *before* the packet is transmitted from the source -* relay to the target relay. -* @event UDPRelayHandler#transmit -* @param {RelayEntry} sourceRelay Source relay -* @param {RelayEntry} targetRelay Target relay -* @param {Buffer} message Message -*/ + * Relay transmission event. + * + * This event is emitted *before* the packet is transmitted from the source + * relay to the target relay. + * @event UDPRelayHandler#transmit + * @param {RelayEntry} sourceRelay Source relay + * @param {RelayEntry} targetRelay Target relay + * @param {Buffer} message Message + */ /** -* Relay destroy event. -* -* This event is emitted *before* a relay and its associated resources are -* freed. -* @event UDPRelayHandler#destroy -* @param {RelayEntry} relay Relay being freed. -*/ + * Relay destroy event. + * + * This event is emitted *before* a relay and its associated resources are + * freed. + * @event UDPRelayHandler#destroy + * @param {RelayEntry} relay Relay being freed. + */ /** -* Relay drop event. -* -* This event is emitted when a packet arrives for relay that we can't transfer -* - usually because of an unknown node ( either sender or target). -* @event UDPRelayHandler#drop -* @param {RelayEntry} sourceRelay Source relay -* @param {RelayEntry} targetRelay Target relay -* @param {NetAddress} sourceAddress Source address -* @param {number} targetPort Target port -* @param {Buffer} message Message -*/ + * Relay drop event. + * + * This event is emitted when a packet arrives for relay that we can't transfer + * - usually because of an unknown node ( either sender or target). + * @event UDPRelayHandler#drop + * @param {RelayEntry} sourceRelay Source relay + * @param {RelayEntry} targetRelay Target relay + * @param {NetAddress} sourceAddress Source address + * @param {number} targetPort Target port + * @param {Buffer} message Message + */ diff --git a/src/relay/udp.remote.registrar.mjs b/src/relay/udp.remote.registrar.mjs index d606500..a40743f 100644 --- a/src/relay/udp.remote.registrar.mjs +++ b/src/relay/udp.remote.registrar.mjs @@ -1,115 +1,115 @@ /* eslint-disable */ -import { HostRepository } from '../hosts/host.repository.mjs' +import { HostRepository } from "../hosts/host.repository.js"; /* eslint-enable */ -import dgram from 'node:dgram' -import assert from 'node:assert' -import logger from '../logger.mjs' -import { requireParam } from '../assertions.mjs' -import * as prometheus from 'prom-client' -import { metricsRegistry } from '../metrics/metrics.registry.mjs' +import dgram from "node:dgram"; +import assert from "node:assert"; +import logger from "../logger.mjs"; +import { requireParam } from "../assertions.mjs"; +import * as prometheus from "prom-client"; +import { metricsRegistry } from "../metrics/metrics.registry.js"; -const log = logger.child({ name: 'UDPRemoteRegistrar' }) +const log = logger.child({ name: "UDPRemoteRegistrar" }); const registerSuccessCounter = new prometheus.Counter({ - name: 'noray_remote_registrar_success', - help: 'Number of successful remote address registrations', - registers: [metricsRegistry] -}) + name: "noray_remote_registrar_success", + help: "Number of successful remote address registrations", + registers: [metricsRegistry], +}); const registerFailCounter = new prometheus.Counter({ - name: 'noray_remote_registrar_fail', - help: 'Number of failed remote address registrations', - registers: [metricsRegistry] -}) + name: "noray_remote_registrar_fail", + help: "Number of failed remote address registrations", + registers: [metricsRegistry], +}); const registerRepatCounter = new prometheus.Counter({ - name: 'noray_remote_registrar_repeat', - help: 'Number of redundant remote address registrations', - registers: [metricsRegistry] -}) + name: "noray_remote_registrar_repeat", + help: "Number of redundant remote address registrations", + registers: [metricsRegistry], +}); /** -* @summary Class for remote address registration over UDP. -* -* @description The UDP remote registrar will listen on a specific port for -* incoming host ID's. If the host ID is valid, it will create a new relay -* for that player and reply a packet saying 'OK'. -* -* Note that if the relay already exists, it will reply anyway, but will not -* create duplicate relays. This helps combatting UDP's unreliable nature - -* clients can just spam the request until they receive a reply. -*/ + * @summary Class for remote address registration over UDP. + * + * @description The UDP remote registrar will listen on a specific port for + * incoming host ID's. If the host ID is valid, it will create a new relay + * for that player and reply a packet saying 'OK'. + * + * Note that if the relay already exists, it will reply anyway, but will not + * create duplicate relays. This helps combatting UDP's unreliable nature - + * clients can just spam the request until they receive a reply. + */ export class UDPRemoteRegistrar { /** @type {dgram.Socket} */ - #socket + #socket; /** @type {HostRepository} */ - #hostRepository + #hostRepository; /** - * Construct instance. - * @param {object} options Options - * @param {HostRepository} options.hostRepository Host repository - * @param {dgram.Socket} [options.socket] Socket - */ - constructor (options) { - this.#hostRepository = requireParam(options.hostRepository) - this.#socket = options.socket ?? dgram.createSocket('udp4') + * Construct instance. + * @param {object} options Options + * @param {HostRepository} options.hostRepository Host repository + * @param {dgram.Socket} [options.socket] Socket + */ + constructor(options) { + this.#hostRepository = requireParam(options.hostRepository); + this.#socket = options.socket ?? dgram.createSocket("udp4"); } /** - * Start listening for incoming requests. - * @param {number} [port=0] Port - * @param {string} [address='0.0.0.0'] Address - * @returns {Promise} - */ - listen (port, address) { - return new Promise(resolve => { - port ??= 0 - address ??= '0.0.0.0' + * Start listening for incoming requests. + * @param {number} [port=0] Port + * @param {string} [address='0.0.0.0'] Address + * @returns {Promise} + */ + listen(port, address) { + return new Promise((resolve) => { + port ??= 0; + address ??= "0.0.0.0"; - this.#socket.on('message', (msg, rinfo) => this.#handle(msg, rinfo)) + this.#socket.on("message", (msg, rinfo) => this.#handle(msg, rinfo)); this.#socket.bind(port, address, () => { - const address = this.#socket.address() - log.info('Listening on %s:%s', address.address, address.port) - resolve() - }) - }) + const address = this.#socket.address(); + log.info("Listening on %s:%s", address.address, address.port); + resolve(); + }); + }); } /** - * Socket listening for requests. - * @type {dgram.Socket} - */ - get socket () { - return this.#socket + * Socket listening for requests. + * @type {dgram.Socket} + */ + get socket() { + return this.#socket; } /** - * @param {Buffer} msg - * @param {dgram.RemoteInfo} rinfo - */ - async #handle (msg, rinfo) { + * @param {Buffer} msg + * @param {dgram.RemoteInfo} rinfo + */ + async #handle(msg, rinfo) { try { - const pid = msg.toString('utf8') - log.debug({ pid, rinfo }, 'Received UDP relay request') + const pid = msg.toString("utf8"); + log.debug({ pid, rinfo }, "Received UDP relay request"); - const host = this.#hostRepository.findByPid(pid) - assert(host, 'Unknown host pid!') + const host = this.#hostRepository.findByPid(pid); + assert(host, "Unknown host pid!"); if (host.rinfo) { // Host has already remote info registered - this.#socket.send('OK', rinfo.port, rinfo.address) - registerRepatCounter.inc() - return + this.#socket.send("OK", rinfo.port, rinfo.address); + registerRepatCounter.inc(); + return; } - host.rinfo = rinfo - this.#socket.send('OK', rinfo.port, rinfo.address) - registerSuccessCounter.inc() + host.rinfo = rinfo; + this.#socket.send("OK", rinfo.port, rinfo.address); + registerSuccessCounter.inc(); } catch (e) { - registerFailCounter.inc() - this.#socket.send(e.message ?? 'Error', rinfo.port, rinfo.address) + registerFailCounter.inc(); + this.#socket.send(e.message ?? "Error", rinfo.port, rinfo.address); } } } diff --git a/src/repository.ts b/src/repository.ts index 90f652f..9a221f7 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -9,7 +9,7 @@ export class UnknownItemError extends Error { } /** * Base class for repositories. */ -export class Repository { +export class Repository { protected items = new Map(); constructor( From cbdc7f1ab619554680fe0f85100b2d54dbdc2712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 23:36:37 +0200 Subject: [PATCH 08/22] kinda port the rest --- src/connection/connection.commands.ts | 8 +- src/hosts/host.commands.mjs | 2 - src/relay/bandwidth.limiter.mjs | 55 -------- src/relay/bandwidth.limiter.ts | 70 ++++++++++ src/relay/constraints.mjs | 79 ----------- src/relay/constraints.ts | 104 ++++++++++++++ src/relay/dynamic.relaying.mjs | 88 ------------ src/relay/dynamic.relaying.ts | 98 +++++++++++++ src/relay/net.address.mjs | 42 ------ src/relay/net.address.ts | 31 +++++ src/relay/relay.entry.mjs | 64 --------- src/relay/relay.entry.ts | 66 +++++++++ src/relay/{relay.mjs => relay.ts} | 44 +++--- src/relay/udp.relay.cleanup.mjs | 29 ---- src/relay/udp.relay.cleanup.ts | 34 +++++ ...relay.handler.mjs => udp.relay.handler.ts} | 95 +++++-------- ....registrar.mjs => udp.remote.registrar.ts} | 74 ++++------ src/relay/udp.socket.pool.mjs | 129 ------------------ src/relay/udp.socket.pool.ts | 127 +++++++++++++++++ src/utils.ts | 26 ++++ 20 files changed, 645 insertions(+), 620 deletions(-) delete mode 100644 src/hosts/host.commands.mjs delete mode 100644 src/relay/bandwidth.limiter.mjs create mode 100644 src/relay/bandwidth.limiter.ts delete mode 100644 src/relay/constraints.mjs create mode 100644 src/relay/constraints.ts delete mode 100644 src/relay/dynamic.relaying.mjs create mode 100644 src/relay/dynamic.relaying.ts delete mode 100644 src/relay/net.address.mjs create mode 100644 src/relay/net.address.ts delete mode 100644 src/relay/relay.entry.mjs create mode 100644 src/relay/relay.entry.ts rename src/relay/{relay.mjs => relay.ts} (68%) delete mode 100644 src/relay/udp.relay.cleanup.mjs create mode 100644 src/relay/udp.relay.cleanup.ts rename src/relay/{udp.relay.handler.mjs => udp.relay.handler.ts} (71%) rename src/relay/{udp.remote.registrar.mjs => udp.remote.registrar.ts} (56%) delete mode 100644 src/relay/udp.socket.pool.mjs create mode 100644 src/relay/udp.socket.pool.ts diff --git a/src/connection/connection.commands.ts b/src/connection/connection.commands.ts index 86dbb45..a376cf4 100644 --- a/src/connection/connection.commands.ts +++ b/src/connection/connection.commands.ts @@ -3,9 +3,9 @@ import { NodeSocketReactor } from "@foxssake/trimsock-node"; import { RemoteInfo } from "node:dgram"; import assert from "node:assert"; import logger from "../logger"; -import { udpRelayHandler } from "../relay/relay.mjs"; -import { RelayEntry } from "../relay/relay.entry.mjs"; -import { NetAddress } from "../relay/net.address.mjs"; +import { udpRelayHandler } from "../relay/relay.js"; +import { RelayEntry } from "../relay/relay.entry.js"; +import { NetAddress } from "../relay/net.address.js"; export function handleConnect(hostRepository: HostRepository) { return function(server: NodeSocketReactor) { @@ -97,7 +97,7 @@ function getRelay(rinfo: RemoteInfo) { const log = logger.child({ name: "getRelay" }); log.trace({ rinfo }, "Ensuring relay for remote"); const relayEntry = udpRelayHandler.createRelay( - new RelayEntry({ address: NetAddress.fromRinfo(rinfo) }), + new RelayEntry({ address: NetAddress.fromRinfo(rinfo), port: rinfo.port }), ); log.trace( diff --git a/src/hosts/host.commands.mjs b/src/hosts/host.commands.mjs deleted file mode 100644 index ac685c7..0000000 --- a/src/hosts/host.commands.mjs +++ /dev/null @@ -1,2 +0,0 @@ - -../metrics/metrics.registry.js diff --git a/src/relay/bandwidth.limiter.mjs b/src/relay/bandwidth.limiter.mjs deleted file mode 100644 index d1e983c..0000000 --- a/src/relay/bandwidth.limiter.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import assert from 'node:assert' -import { requireParam } from '../assertions.mjs' -import { time } from '../utils.mjs' - -/** -* Generic component to limit bandwidth usage. -* -* Internally, the bandwidth limiter divides chunks into time slices of -* `interval` length. Every time slice is allocated a certain amount of data -* transfer based on the max traffic specified in bytes/sec ( i.e. if interval -* is 1/8th of a second, then the max traffic for a time slice is 1/8th of the -* max traffic ). -* -* For any traffic that is within the limit, the counter is increased and the -* validation passes. Once the time slice expires, the counter is reset and a -* new one is started. -*/ -export class BandwidthLimiter { - #interval - #maxTraffic - #traffic - #lastInterval - - /** - * Construct instance. - * @param {object} options Options - * @param {number} options.maxTraffic Traffic limit in bytes/sec - * @param {number} options.interval Traffic interval length in seconds - */ - constructor (options) { - this.#interval = options.interval ?? 1 - this.#maxTraffic = requireParam(options.maxTraffic) - this.#traffic = 0 - this.#lastInterval = time() - } - - /** - * Validate that a given size of data fits into the bandwidth limit. - * @param {number} size Data size - * @throws if traffic is over limit - */ - validate (size) { - // If we're past the last interval, start a new one - if (time() > this.#lastInterval + this.#interval) { - this.#lastInterval = time() - this.#traffic = size - return - } - - const limit = this.#maxTraffic * this.#interval - assert(this.#traffic + size <= limit, 'Bandwidth limit reached!') - - this.#traffic += size - } -} diff --git a/src/relay/bandwidth.limiter.ts b/src/relay/bandwidth.limiter.ts new file mode 100644 index 0000000..deecdac --- /dev/null +++ b/src/relay/bandwidth.limiter.ts @@ -0,0 +1,70 @@ +import assert from "node:assert"; +import { requireParam } from "../assertions"; +import { formatBandwidth, time } from "../utils"; + +/** + * Constructor options for [BandwidthLimiter] + */ +export interface BandwithLimiterOptions { + /** Traffic limit in bytes/sec */ + maxTraffic: number; + + /** Traffic interval length in seconds */ + interval?: number; +} + +export class BandwidthLimitExceededError extends Error { } + +/** + * Generic component to limit bandwidth usage. + * + * Internally, the bandwidth limiter divides chunks into time slices of + * `interval` length. Every time slice is allocated a certain amount of data + * transfer based on the max traffic specified in bytes/sec ( i.e. if interval + * is 1/8th of a second, then the max traffic for a time slice is 1/8th of the + * max traffic ). + * + * For any traffic that is within the limit, the counter is increased and the + * validation passes. Once the time slice expires, the counter is reset and a + * new one is started. + */ +export class BandwidthLimiter { + private interval: number = 1; + private maxTraffic: number; + + private traffic: number = 0; + private lastInterval: number = time(); + + /** + * Construct instance. + */ + constructor(options: BandwithLimiterOptions) { + this.interval = options.interval ?? 1; + this.maxTraffic = options.maxTraffic; + this.traffic = 0; + this.lastInterval = time(); + } + + /** + * Validate that a given size of data fits into the bandwidth limit. + * @throws if traffic is over limit + */ + validate(size: number) { + // If we're past the last interval, start a new one + if (time() > this.lastInterval + this.interval) { + this.lastInterval = time(); + this.traffic = size; + return; + } + + const limit = this.maxTraffic * this.interval; + if (this.traffic + size > limit) + // TODO: Optionally have a configurable `id` field on the class, which can + // be displayed in this exception for readability + throw new BandwidthLimitExceededError( + `Bandwidth limit of ${formatBandwidth(this.maxTraffic)} exceeded!`, + ); + + this.traffic += size; + } +} diff --git a/src/relay/constraints.mjs b/src/relay/constraints.mjs deleted file mode 100644 index 6849bc1..0000000 --- a/src/relay/constraints.mjs +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable */ -import { BandwidthLimiter } from './bandwidth.limiter.mjs' -import { UDPRelayHandler } from './udp.relay.handler.mjs' -/* eslint-enable */ -import assert from 'node:assert' -import { time } from '../utils.mjs' - -/** -* Limit the bandwidth on every relay individually. -* @param {UDPRelayHandler} relayHandler Relay handler -* @param {number} traffic Traffic limit in bytes/sec -* @param {number} [interval=1] Limit interval in seconds -*/ -export function constrainIndividualBandwidth (relayHandler, traffic, interval) { - const limiters = new Map() - - relayHandler.on('transmit', (source, _target, message) => { - if (!limiters.has(source.id)) { - limiters.set(source.id, new BandwidthLimiter({ - maxTraffic: traffic, - interval: interval ?? 1 - })) - } - - const limiter = limiters.get(source.id) - limiter.validate(message.length) - }) - - relayHandler.on('destroy', relay => { - limiters.delete(relay.id) - }) -} - -/** -* Limit the bandwidth on every relay globally. -* @param {UDPRelayHandler} relayHandler Relay handler -* @param {number} traffic Traffic limit in bytes/sec -* @param {number} [interval=1] Limit interval in seconds -*/ -export function constrainGlobalBandwidth (relayHandler, traffic, interval) { - const limiter = new BandwidthLimiter({ - maxTraffic: traffic, - interval: interval ?? 1 - }) - - relayHandler.on('transmit', (_source, _target, message) => { - limiter.validate(message.length) - }) -} - -/** -* Block all traffic on relays after they've been active for a given duration. -* @param {UDPRelayHandler} relayHandler Relay handler -* @param {number} duration Duration in seconds -*/ -export function constrainLifetime (relayHandler, duration) { - relayHandler.on('transmit', (source, _target, _message) => { - assert(time() - source.created < duration, 'Relay has hit lifetime duration limit!') - }) -} - -/** -* Block all traffic on relays after they reached a given amount of traffic. -* @param {UDPRelayHandler} relayHandler Relay handler -* @param {number} traffic Maximum traffic in bytes -*/ -export function constrainTraffic (relayHandler, traffic) { - const relayTraffic = new Map() - - relayHandler.on('transmit', (source, _target, message) => { - const id = source.id - relayTraffic.set(id, (relayTraffic.get(id) ?? 0) + message.byteLength) - assert(relayTraffic.get(id) < traffic, 'Relay has hit lifetime traffic limit!') - }) - - relayHandler.on('destroy', relay => { - relayTraffic.delete(relay.id) - }) -} diff --git a/src/relay/constraints.ts b/src/relay/constraints.ts new file mode 100644 index 0000000..35fa712 --- /dev/null +++ b/src/relay/constraints.ts @@ -0,0 +1,104 @@ +import { BandwidthLimiter } from "./bandwidth.limiter"; +import { UDPRelayHandler } from "./udp.relay.handler.js"; +import assert from "node:assert"; +import { time } from "../utils"; + +/** + * Limit the bandwidth on every relay individually. + * + * @param relayHandler Relay handler + * @param traffic Traffic limit in bytes/sec + * @param interval Limit interval in seconds + */ +export function constrainIndividualBandwidth( + relayHandler: UDPRelayHandler, + traffic: number, + interval: number = 1, +) { + const limiters = new Map(); + + relayHandler.on("transmit", (source, _target, message) => { + if (!limiters.has(source.id)) { + limiters.set( + source.id, + new BandwidthLimiter({ + maxTraffic: traffic, + interval: interval ?? 1, + }), + ); + } + + const limiter = limiters.get(source.id); + limiter.validate(message.length); + }); + + relayHandler.on("destroy", (relay) => { + limiters.delete(relay.id); + }); +} + +/** + * Limit the bandwidth on every relay globally. + * + * @param relayHandler Relay handler + * @param traffic Traffic limit in bytes/sec + * @param interval Limit interval in seconds + */ +export function constrainGlobalBandwidth( + relayHandler: UDPRelayHandler, + traffic: number, + interval: number = 1, +) { + const limiter = new BandwidthLimiter({ + maxTraffic: traffic, + interval: interval ?? 1, + }); + + relayHandler.on("transmit", (_source, _target, message) => { + limiter.validate(message.length); + }); +} + +/** + * Block all traffic on relays after they've been active for a given duration. + * @param relayHandler Relay handler + * @param duration Duration in seconds + */ +export function constrainLifetime( + relayHandler: UDPRelayHandler, + duration: number, +) { + relayHandler.on("transmit", (source, _target, _message) => { + // TODO: Prefer custom exception + assert( + time() - source.created < duration, + "Relay has hit lifetime duration limit!", + ); + }); +} + +/** + * Block all traffic on relays after they reached a given amount of traffic. + * @param relayHandler Relay handler + * @param traffic Maximum traffic in bytes + */ +export function constrainTraffic( + relayHandler: UDPRelayHandler, + traffic: number, +) { + const relayTraffic = new Map(); + + relayHandler.on("transmit", (source, _target, message) => { + const id = source.id; + relayTraffic.set(id, (relayTraffic.get(id) ?? 0) + message.byteLength); + // TODO: Prefer custom exception + assert( + relayTraffic.get(id) < traffic, + "Relay has hit lifetime traffic limit!", + ); + }); + + relayHandler.on("destroy", (relay) => { + relayTraffic.delete(relay.id); + }); +} diff --git a/src/relay/dynamic.relaying.mjs b/src/relay/dynamic.relaying.mjs deleted file mode 100644 index abf9d49..0000000 --- a/src/relay/dynamic.relaying.mjs +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable */ -import { NetAddress } from './net.address.mjs' -import { UDPRelayHandler } from './udp.relay.handler.mjs' -/* eslint-enable */ -import logger from '../logger.mjs' -import { RelayEntry } from './relay.entry.mjs' - -const log = logger.child({ name: 'DynamicRelaying' }) - -/** -* Implementation for dynamic relaying. -* -* Whenever an unknown client tries to send data to a known host through its -* relay address, dynamic relaying will create a new relay. -* -* While it's waiting for the relay to be created, it will buffer any incoming -* data and send it all once the relay is created. -*/ -export class DynamicRelaying { - /** @type {Map} */ - #buffers = new Map() - - /** - * Apply dynamic relay creation to relay handler. - * @param {UDPRelayHandler} relayHandler Relay handler - */ - apply (relayHandler) { - relayHandler.on('drop', - (senderRelay, targetRelay, senderAddress, targetPort, message) => - this.#handle(relayHandler, senderRelay, targetRelay, senderAddress, targetPort, message) - ) - } - - /** - * @param {UDPRelayHandler} relayHandler - * @param {RelayEntry} senderRelay - * @param {RelayEntry} targetRelay - * @param {NetAddress} senderAddress - * @param {number} targetPort - * @param {Buffer} message - */ - async #handle (relayHandler, senderRelay, targetRelay, senderAddress, targetPort, message) { - // Unknown host or client already has relay, ignore - if (senderRelay || !targetRelay) { - return - } - - const key = senderAddress.toString() + '>' + targetPort - - // We're already buffering for client, save data end return - if (this.#buffers.has(key)) { - this.#buffers.get(key).push(message) - return - } - - // No buffer for client yet, start buffering and create relay - log.info( - { from: senderAddress, to: targetRelay.address }, - 'Creating dynamic relay' - ) - this.#buffers.set(key, [message]) - const port = relayHandler.socketPool.getPort() - const relay = new RelayEntry({ - address: senderAddress, - port - }) - await relayHandler.createRelay(relay) - - log.info( - { relay }, - 'Relay created, sending %d packets', - this.#buffers.get(key)?.length ?? 0 - ) - this.#buffers.get(key)?.forEach(msg => - relayHandler.relay(msg, senderAddress, targetPort) - ) - - this.#buffers.delete(key) - } -} - -/** -* Apply dynamic relaying to relay handler. -* @param {UDPRelayHandler} relayHandler Relay handler -*/ -export function useDynamicRelay (relayHandler) { - new DynamicRelaying().apply(relayHandler) -} diff --git a/src/relay/dynamic.relaying.ts b/src/relay/dynamic.relaying.ts new file mode 100644 index 0000000..85cea4c --- /dev/null +++ b/src/relay/dynamic.relaying.ts @@ -0,0 +1,98 @@ +import { NetAddress } from "./net.address"; +import { UDPRelayHandler } from "./udp.relay.handler.js"; +import logger from "../logger"; +import { RelayEntry } from "./relay.entry.js"; + +const log = logger.child({ name: "DynamicRelaying" }); + +/** + * Implementation for dynamic relaying. + * + * Whenever an unknown client tries to send data to a known host through its + * relay address, dynamic relaying will create a new relay. + * + * While it's waiting for the relay to be created, it will buffer any incoming + * data and send it all once the relay is created. + */ +export class DynamicRelaying { + private buffers = new Map(); + + /** + * Apply dynamic relay creation to relay handler. + */ + apply(relayHandler: UDPRelayHandler) { + relayHandler.on( + "drop", + (senderRelay, targetRelay, senderAddress, targetPort, message) => + this.handle( + relayHandler, + senderRelay, + targetRelay, + senderAddress, + targetPort, + message, + ), + ); + } + + /** + * @param {UDPRelayHandler} relayHandler + * @param {RelayEntry} senderRelay + * @param {RelayEntry} targetRelay + * @param {NetAddress} senderAddress + * @param {number} targetPort + * @param {Buffer} message + */ + private async handle( + relayHandler: UDPRelayHandler, + senderRelay: RelayEntry, + targetRelay: RelayEntry, + senderAddress: NetAddress, + targetPort: number, + message: Buffer, + ) { + // Unknown host or client already has relay, ignore + if (senderRelay || !targetRelay) { + return; + } + + const key = senderAddress.toString() + ">" + targetPort; + + // We're already buffering for client, save data end return + if (this.buffers.has(key)) { + this.buffers.get(key)?.push(message); + return; + } + + // No buffer for client yet, start buffering and create relay + log.info( + { from: senderAddress, to: targetRelay.address }, + "Creating dynamic relay", + ); + this.buffers.set(key, [message]); + const port = relayHandler.socketPool.getPort(); + const relay = new RelayEntry({ + address: senderAddress, + port, + }); + await relayHandler.createRelay(relay); + + log.info( + { relay }, + "Relay created, sending %d packets", + this.buffers.get(key)?.length ?? 0, + ); + this.buffers + .get(key) + ?.forEach((msg) => relayHandler.relay(msg, senderAddress, targetPort)); + + this.buffers.delete(key); + } +} + +/** + * Apply dynamic relaying to relay handler. + */ +export function useDynamicRelay(relayHandler: UDPRelayHandler) { + new DynamicRelaying().apply(relayHandler); +} diff --git a/src/relay/net.address.mjs b/src/relay/net.address.mjs deleted file mode 100644 index 32281f5..0000000 --- a/src/relay/net.address.mjs +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable */ -import dgram from 'node:dgram' -/* eslint-enable */ - -export class NetAddress { - /** @type {string} */ - address - - /** @type {number} */ - port - - /** - * @param {NetAddress} options - */ - constructor (options) { - options && Object.assign(this, options) - } - - /** - * Check for equality - * @param {NetAddress} other Other - * @returns {boolean} True if equal - */ - equals (other) { - return this.address === other.address && this.port === other.port - } - - toString () { - return `${this.address}:${this.port}` - } - - /** - * @param {dgram.RemoteInfo} rinfo - * @returns {NetAddress} - */ - static fromRinfo (rinfo) { - return new NetAddress({ - address: rinfo.address, - port: rinfo.port - }) - } -} diff --git a/src/relay/net.address.ts b/src/relay/net.address.ts new file mode 100644 index 0000000..9b9cd07 --- /dev/null +++ b/src/relay/net.address.ts @@ -0,0 +1,31 @@ +import dgram from "node:dgram"; + +export interface NetAddressData { + address: string; + port: number; +} + +export class NetAddress implements NetAddressData { + address: string; + port: number; + + constructor(options: NetAddressData) { + this.address = options.address; + this.port = options.port; + } + + equals(other: NetAddressData) { + return this.address === other.address && this.port === other.port; + } + + toString() { + return `${this.address}:${this.port}`; + } + + static fromRinfo(rinfo: dgram.RemoteInfo): NetAddress { + return new NetAddress({ + address: rinfo.address, + port: rinfo.port, + }); + } +} diff --git a/src/relay/relay.entry.mjs b/src/relay/relay.entry.mjs deleted file mode 100644 index 7bb6ee6..0000000 --- a/src/relay/relay.entry.mjs +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable */ -import { NetAddress } from './net.address.mjs' -/* eslint-enable */ -import { time } from '../utils.mjs' - -/** -* Entry for the relay translation tables. -*/ -export class RelayEntry { - /** - * The port on which we've received traffic - * @type {number} - */ - port - - /** - * The target address where traffic should be forwarded - * @type {NetAddress} - */ - address - - /** - * Time the relay was last used to send data. - * @type {number} - */ - lastSent = 0 - - /** - * Time the relay last received traffic on its port. - * @type {number} - */ - lastReceived = 0 - - /** - * Time the relay was created. - * @type {number} - */ - created = time() - - /** - * Construct entry - * @param {RelayEntry} options Options - */ - constructor (options) { - Object.assign(this, options) - } - - /** - * Check for equality - * @param {RelayEntry} other Other - * @returns {boolean} True if equal - */ - equals (other) { - return this.address.equals(other.address) - } - - /** - * Relay identifier - * @type {string} - */ - get id () { - return `${this.address}@${this.port}` - } -} diff --git a/src/relay/relay.entry.ts b/src/relay/relay.entry.ts new file mode 100644 index 0000000..614f4e1 --- /dev/null +++ b/src/relay/relay.entry.ts @@ -0,0 +1,66 @@ +import { NetAddress } from "./net.address"; +import { time } from "../utils"; + +export interface RelayEntryData { + /** + * The port on which we've received traffic + */ + port: number; + + /** + * The target address where traffic should be forwarded + */ + address: NetAddress; +} + +/** + * Entry for the relay translation tables. + */ +export class RelayEntry implements RelayEntryData { + /** + * The port on which we've received traffic + */ + port: number; + + /** + * The target address where traffic should be forwarded + */ + address: NetAddress; + + /** + * Time the relay was last used to send data. + */ + lastSent = 0; + + /** + * Time the relay last received traffic on its port. + */ + lastReceived = 0; + + /** + * Time the relay was created. + */ + created = time(); + + /** + * Construct entry + */ + constructor(options: RelayEntryData) { + this.port = options.port; + this.address = options.address; + } + + /** + * Check for equality + */ + equals(other: RelayEntry): boolean { + return this.address.equals(other.address); + } + + /** + * Relay identifier + */ + get id(): string { + return `${this.address}@${this.port}`; + } +} diff --git a/src/relay/relay.mjs b/src/relay/relay.ts similarity index 68% rename from src/relay/relay.mjs rename to src/relay/relay.ts index 1a71b51..736c5c9 100644 --- a/src/relay/relay.mjs +++ b/src/relay/relay.ts @@ -1,19 +1,19 @@ -import { config } from "../config.mjs"; +import { config } from "../config"; import { constrainGlobalBandwidth, constrainIndividualBandwidth, constrainLifetime, constrainTraffic, -} from "./constraints.mjs"; -import { UDPRelayHandler } from "./udp.relay.handler.mjs"; +} from "./constraints"; +import { UDPRelayHandler } from "./udp.relay.handler.js"; import { Noray } from "../noray.js"; -import { cleanupUdpRelayTable } from "./udp.relay.cleanup.mjs"; -import logger from "../logger.mjs"; -import { formatByteSize, formatDuration } from "../utils.mjs"; -import { UDPRemoteRegistrar } from "./udp.remote.registrar.mjs"; +import { cleanupUdpRelayTable } from "./udp.relay.cleanup.js"; +import logger from "../logger"; +import { formatByteSize, formatDuration } from "../utils"; +import { UDPRemoteRegistrar } from "./udp.remote.registrar.js"; import { hostRepository } from "../hosts/host.js"; -import { useDynamicRelay } from "./dynamic.relaying.mjs"; -import { UDPSocketPool } from "./udp.socket.pool.mjs"; +import { useDynamicRelay } from "./dynamic.relaying.js"; +import { UDPSocketPool } from "./udp.socket.pool.js"; export const udpSocketPool = new UDPSocketPool(); @@ -30,11 +30,11 @@ const log = logger.child({ name: "mod:relay" }); Noray.hook(async (noray) => { log.info( "Starting periodic UDP relay cleanup job, running every %s", - formatDuration(config.udpRelay.cleanupInterval), + formatDuration(config.udpRelay.cleanupInterval!!), ); const cleanupJob = setInterval( - () => cleanupUdpRelayTable(udpRelayHandler, config.udpRelay.timeout), - config.udpRelay.cleanupInterval * 1000, + () => cleanupUdpRelayTable(udpRelayHandler, config.udpRelay.timeout!!), + config.udpRelay.cleanupInterval!! * 1000, ); log.info( @@ -43,9 +43,9 @@ Noray.hook(async (noray) => { ); udpRemoteRegistrar.listen(config.udpRelay.registrarPort); - log.info("Binding %d ports for relaying", config.udpRelay.ports.length); + log.info("Binding %d ports for relaying", config.udpRelay.ports!!.length); - for (const port of config.udpRelay.ports) { + for (const port of config.udpRelay.ports!!) { log.trace("Binding port %d for relay", port); try { await udpSocketPool.allocatePort(port); @@ -56,29 +56,29 @@ Noray.hook(async (noray) => { log.info( "Limiting relay bandwidth to %s/s and global bandwidth to %s/s", - formatByteSize(config.udpRelay.maxIndividualTraffic), - formatByteSize(config.udpRelay.maxGlobalTraffic), + formatByteSize(config.udpRelay.maxIndividualTraffic!!), + formatByteSize(config.udpRelay.maxGlobalTraffic!!), ); constrainIndividualBandwidth( udpRelayHandler, - config.udpRelay.maxIndividualTraffic, + config.udpRelay.maxIndividualTraffic!!, config.udpRelay.trafficInterval, ); constrainGlobalBandwidth( udpRelayHandler, - config.udpRelay.maxGlobalTraffic, + config.udpRelay.maxGlobalTraffic!!, config.udpRelay.trafficInterval, ); log.info( "Blocking relay traffic after %s or %s", - formatDuration(config.udpRelay.maxLifetimeDuration), - formatByteSize(config.udpRelay.maxLifetimeTraffic), + formatDuration(config.udpRelay.maxLifetimeDuration!!), + formatByteSize(config.udpRelay.maxLifetimeTraffic!!), ); - constrainLifetime(udpRelayHandler, config.udpRelay.maxLifetimeDuration); - constrainTraffic(udpRelayHandler, config.udpRelay.maxLifetimeTraffic); + constrainLifetime(udpRelayHandler, config.udpRelay.maxLifetimeDuration!!); + constrainTraffic(udpRelayHandler, config.udpRelay.maxLifetimeTraffic!!); log.info("Applying dynamic relaying"); useDynamicRelay(udpRelayHandler); diff --git a/src/relay/udp.relay.cleanup.mjs b/src/relay/udp.relay.cleanup.mjs deleted file mode 100644 index bdf757c..0000000 --- a/src/relay/udp.relay.cleanup.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable */ -import { UDPRelayHandler } from "./udp.relay.handler.mjs"; -/* eslint-enable */ -import { time } from "../utils.mjs"; -import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry.js"; - -const expiredRelayCounter = new prometheus.Counter({ - name: "noray_relay_expired", - help: "Count of expired relays", - registers: [metricsRegistry], -}); - -/** - * Remove idle relays. - * @param {UDPRelayHandler} relayHandler Relay handler - * @param {number} timeout Maximum relay age in seconds - */ -export function cleanupUdpRelayTable(relayHandler, timeout) { - const timeCutoff = time() - timeout; - - relayHandler.relayTable - .map((relay) => [relay, Math.max(relay.lastSent, relay.lastReceived)]) - .filter(([_, lastTraffic]) => lastTraffic <= timeCutoff) - .forEach(([relay, _]) => { - relayHandler.freeRelay(relay); - expiredRelayCounter.inc(); - }); -} diff --git a/src/relay/udp.relay.cleanup.ts b/src/relay/udp.relay.cleanup.ts new file mode 100644 index 0000000..9498a64 --- /dev/null +++ b/src/relay/udp.relay.cleanup.ts @@ -0,0 +1,34 @@ +import { UDPRelayHandler } from "./udp.relay.handler.js"; +import { time } from "../utils"; +import * as prometheus from "prom-client"; +import { metricsRegistry } from "../metrics/metrics.registry"; +import { RelayEntry } from "./relay.entry"; + +const expiredRelayCounter = new prometheus.Counter({ + name: "noray_relay_expired", + help: "Count of expired relays", + registers: [metricsRegistry], +}); + +/** + * Remove idle relays. + * @param relayHandler Relay handler + * @param timeout Maximum relay age in seconds + */ +export function cleanupUdpRelayTable( + relayHandler: UDPRelayHandler, + timeout: number, +) { + const timeCutoff = time() - timeout; + + relayHandler.relayTable + .filter((relay) => getRelayLastActivity(relay) <= timeCutoff) + .forEach((relay) => { + relayHandler.freeRelay(relay); + expiredRelayCounter.inc(); + }); +} + +function getRelayLastActivity(relay: RelayEntry): number { + return Math.max(relay.lastSent, relay.lastReceived); +} diff --git a/src/relay/udp.relay.handler.mjs b/src/relay/udp.relay.handler.ts similarity index 71% rename from src/relay/udp.relay.handler.mjs rename to src/relay/udp.relay.handler.ts index 23d5dad..828d698 100644 --- a/src/relay/udp.relay.handler.mjs +++ b/src/relay/udp.relay.handler.ts @@ -1,13 +1,11 @@ -/* eslint-disable */ -import { RelayEntry } from "./relay.entry.mjs"; -/* eslint-enable */ -import { NetAddress } from "./net.address.mjs"; -import { UDPSocketPool } from "./udp.socket.pool.mjs"; -import { time } from "../utils.mjs"; +import { RelayEntry } from "./relay.entry"; +import { NetAddress } from "./net.address"; +import { UDPSocketPool } from "./udp.socket.pool.js"; +import { time } from "../utils"; import { EventEmitter } from "node:events"; -import logger from "../logger.mjs"; +import logger from "../logger"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry.js"; +import { metricsRegistry } from "../metrics/metrics.registry"; const log = logger.child({ name: "UDPRelayHandler" }); @@ -35,6 +33,10 @@ const activeRelayGauge = new prometheus.Gauge({ registers: [metricsRegistry], }); +export interface UDPRelayHandlerOptions { + socketPool?: UDPSocketPool; +} + /** * Class implementing the actual relay logic. * @@ -52,21 +54,20 @@ const activeRelayGauge = new prometheus.Gauge({ * to Host. This way, Client will always appear as Noray:2 to the Host. */ export class UDPRelayHandler extends EventEmitter { - /** @type {UDPSocketPool} */ - #socketPool; - - /** @type {RelayEntry[]} */ - #relayTable = []; + /** + * Socketp pool used for relays. + */ + public readonly socketPool: UDPSocketPool; /** - * Construct instance. - * @param {object} options Options - * @param {UDPSocketPool} [options.socketPool] Socket pool + * Relay table used for relaying. */ - constructor(options) { + public readonly relayTable: RelayEntry[] = []; + + constructor(options: UDPRelayHandlerOptions) { super(); - this.#socketPool = options?.socketPool ?? new UDPSocketPool(); + this.socketPool = options?.socketPool ?? new UDPSocketPool(); } /** @@ -75,29 +76,27 @@ export class UDPRelayHandler extends EventEmitter { * If there's already a relay for the address, returns that. * NOTE: This modifies the incoming relay and returns the same instance. * - * @param {RelayEntry} relay Relay - * @return {RelayEntry} Resulting relay * @fires UDPRelayHandler#create */ - createRelay(relay) { + createRelay(relay: RelayEntry): RelayEntry { log.debug({ relay }, "Creating relay"); if (this.hasRelay(relay)) { // We already have this relay entry log.trace({ relay }, "Relay already exists, ignoring"); - return this.#relayTable.find((e) => e.equals(relay)); + return this.relayTable.find((e) => e.equals(relay))!!; } - relay.port = this.#socketPool.getPort(); + relay.port = this.socketPool.getPort(); this.emit("create", relay); - const socket = this.#socketPool.getSocket(relay.port); + const socket = this.socketPool.getSocket(relay.port)!!; socket.removeAllListeners("message").on("message", (msg, rinfo) => { this.relay(msg, NetAddress.fromRinfo(rinfo), relay.port); }); relay.lastReceived = time(); relay.created = time(); - this.#relayTable.push(relay); + this.relayTable.push(relay); log.trace({ relay }, "Relay created"); activeRelayGauge.inc(); @@ -109,29 +108,25 @@ export class UDPRelayHandler extends EventEmitter { * Check if relay already exists in the table. * * NOTE: This only compares the addresses, not the allocated port. - * @param {RelayEntry} relay Relay - * @returns {boolean} True if relay already exists */ - hasRelay(relay) { - return this.#relayTable.find((e) => e.equals(relay)) !== undefined; + hasRelay(relay: RelayEntry): boolean { + return this.relayTable.find((e) => e.equals(relay)) !== undefined; } /** * Free a relay entry, removing it from the table and freeing any associated resources. - * @param {RelayEntry} relay Relay - * @returns {boolean} True if a relay was freed * @fires UDPRelayHandler#destroy */ - freeRelay(relay) { - const idx = this.#relayTable.findIndex((e) => e.equals(relay)); + freeRelay(relay: RelayEntry): boolean { + const idx = this.relayTable.findIndex((e) => e.equals(relay)); if (idx < 0) { return false; } this.emit("destroy", relay); - this.#socketPool.returnPort(relay.port); - this.#relayTable = this.#relayTable.filter((_, i) => i !== idx); + this.socketPool.returnPort(relay.port); + this.relayTable = this.relayTable.filter((_, i) => i !== idx); // TODO: Ugh activeRelayGauge.dec(); @@ -149,21 +144,19 @@ export class UDPRelayHandler extends EventEmitter { /** * Relay a message from a given sender to target. - * @param {Buffer} msg Message - * @param {NetAddress} sender Sender address - * @param {number} target Target port - * @returns {Promise} True on success + * * @fires UDPRelayHandler#transmit * @fires UDPRelayHandler#drop */ - relay(msg, sender, target) { + // TODO: Why was the return type documented as Promise? + relay(msg: Buffer, sender: NetAddress, target: number): boolean { const measure = relayDurationHistogram.startTimer(); - const senderRelay = this.#relayTable.find( + const senderRelay = this.relayTable.find( (r) => r.address.port === sender.port && r.address.address === sender.address, ); - const targetRelay = this.#relayTable.find((r) => r.port === target); + const targetRelay = this.relayTable.find((r) => r.port === target); if (!senderRelay || !targetRelay) { // We don't have a relay for the sender, target, or both @@ -175,7 +168,7 @@ export class UDPRelayHandler extends EventEmitter { return false; } - const socket = this.#socketPool.getSocket(senderRelay.port); + const socket = this.socketPool.getSocket(senderRelay.port); if (!socket) { // For some reason we don't have the socket return false; @@ -194,22 +187,6 @@ export class UDPRelayHandler extends EventEmitter { return true; } - - /** - * Socket pool used for relays. - * @type {UDPSocketPool} - */ - get socketPool() { - return this.#socketPool; - } - - /** - * Relay table used for relays. - * @type {RelayEntry[]} - */ - get relayTable() { - return [...this.#relayTable]; - } } /** diff --git a/src/relay/udp.remote.registrar.mjs b/src/relay/udp.remote.registrar.ts similarity index 56% rename from src/relay/udp.remote.registrar.mjs rename to src/relay/udp.remote.registrar.ts index a40743f..54bd533 100644 --- a/src/relay/udp.remote.registrar.mjs +++ b/src/relay/udp.remote.registrar.ts @@ -1,12 +1,10 @@ -/* eslint-disable */ -import { HostRepository } from "../hosts/host.repository.js"; -/* eslint-enable */ +import { HostRepository } from "../hosts/host.repository"; import dgram from "node:dgram"; import assert from "node:assert"; -import logger from "../logger.mjs"; -import { requireParam } from "../assertions.mjs"; +import logger from "../logger"; +import { requireParam } from "../assertions"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry.js"; +import { metricsRegistry } from "../metrics/metrics.registry"; const log = logger.child({ name: "UDPRemoteRegistrar" }); @@ -28,6 +26,11 @@ const registerRepatCounter = new prometheus.Counter({ registers: [metricsRegistry], }); +export interface UDPRemoteRegistrarOptions { + hostRepository: HostRepository; + socket?: dgram.Socket; +} + /** * @summary Class for remote address registration over UDP. * @@ -40,76 +43,53 @@ const registerRepatCounter = new prometheus.Counter({ * clients can just spam the request until they receive a reply. */ export class UDPRemoteRegistrar { - /** @type {dgram.Socket} */ - #socket; - - /** @type {HostRepository} */ - #hostRepository; - /** - * Construct instance. - * @param {object} options Options - * @param {HostRepository} options.hostRepository Host repository - * @param {dgram.Socket} [options.socket] Socket + * Socket listening for requests. */ - constructor(options) { - this.#hostRepository = requireParam(options.hostRepository); - this.#socket = options.socket ?? dgram.createSocket("udp4"); + public readonly socket: dgram.Socket; + + private hostRepository: HostRepository; + + constructor(options: UDPRemoteRegistrarOptions) { + this.hostRepository = requireParam(options.hostRepository); + this.socket = options.socket ?? dgram.createSocket("udp4"); } /** * Start listening for incoming requests. - * @param {number} [port=0] Port - * @param {string} [address='0.0.0.0'] Address - * @returns {Promise} */ - listen(port, address) { + listen(port: number = 0, address: string = "0.0.0.0"): Promise { return new Promise((resolve) => { - port ??= 0; - address ??= "0.0.0.0"; - - this.#socket.on("message", (msg, rinfo) => this.#handle(msg, rinfo)); - this.#socket.bind(port, address, () => { - const address = this.#socket.address(); + this.socket.on("message", (msg, rinfo) => this.handle(msg, rinfo)); + this.socket.bind(port, address, () => { + const address = this.socket.address(); log.info("Listening on %s:%s", address.address, address.port); resolve(); }); }); } - /** - * Socket listening for requests. - * @type {dgram.Socket} - */ - get socket() { - return this.#socket; - } - - /** - * @param {Buffer} msg - * @param {dgram.RemoteInfo} rinfo - */ - async #handle(msg, rinfo) { + private async handle(msg: Buffer, rinfo: dgram.RemoteInfo) { try { const pid = msg.toString("utf8"); log.debug({ pid, rinfo }, "Received UDP relay request"); - const host = this.#hostRepository.findByPid(pid); + const host = this.hostRepository.findByPid(pid); assert(host, "Unknown host pid!"); if (host.rinfo) { // Host has already remote info registered - this.#socket.send("OK", rinfo.port, rinfo.address); + this.socket.send("OK", rinfo.port, rinfo.address); registerRepatCounter.inc(); return; } host.rinfo = rinfo; - this.#socket.send("OK", rinfo.port, rinfo.address); + this.socket.send("OK", rinfo.port, rinfo.address); registerSuccessCounter.inc(); - } catch (e) { + } catch (e: any) { registerFailCounter.inc(); - this.#socket.send(e.message ?? "Error", rinfo.port, rinfo.address); + this.socket.send(e.message ?? "Error", rinfo.port, rinfo.address); } } } diff --git a/src/relay/udp.socket.pool.mjs b/src/relay/udp.socket.pool.mjs deleted file mode 100644 index c5ddd86..0000000 --- a/src/relay/udp.socket.pool.mjs +++ /dev/null @@ -1,129 +0,0 @@ -import assert from 'node:assert' -import dgram from 'node:dgram' - -/** -* Class to manage and allocate UDP ports as needed. -* -* The socket pool is used during UDP relaying. We need to both listen on and -* send traffic through multiple ports during relay. The socket pool ensures -* that these sockets are always available. -* -* This is a low-level interface, concerned only with the allocation, storage -* and deallocation of ports and by extension, UDP sockets. -*/ -export class UDPSocketPool { - /** - * Port to socket - * @type {Map} - */ - #sockets = new Map() - - /** - * Free ports - * @type {number[]} - */ - #freePorts = [] - - /** - * Allocate a new port for relaying. - * - * If port is unset or 0, a random port will be picked by the OS. - * @param {number} [port=0] Port to allocate - * @returns {Promise} Allocated port - * @throws if allocation fails - */ - allocatePort (port) { - return new Promise((resolve, reject) => { - const socket = dgram.createSocket('udp4') - socket.once('error', reject) - socket.bind(port ?? 0, () => { - port = this.addSocket(socket) - resolve(port) - }) - }) - } - - /** - * Close the socket associated with the given port. - * - * Does nothing if the port is not managed by the relay. - * @param {number} port Port - */ - deallocatePort (port) { - this.#sockets.get(port)?.close() - this.#sockets.delete(port) - this.#freePorts = this.#freePorts.filter(p => p !== port) - } - - /** - * Get socket listening on port. - * @param {number} port Port - * @returns {dgram.Socket|undefined} Socket - */ - getSocket (port) { - return this.#sockets.get(port) - } - - /** - * Add an already listening socket to use for relaying. - * @param {dgram.Socket} socket Socket - * @returns {number} Relay port - */ - addSocket (socket) { - const port = socket.address().port - this.#sockets.set(port, socket) - this.#freePorts.push(port) - - return port - } - - /** - * Get a free port to use for relaying. - * - * The resulting port can be converted into a socket using `getSocket`. - * @returns {number} - * @throws if no free ports are available - */ - getPort () { - assert(this.#freePorts.length > 0, 'No more free ports!') - return this.#freePorts.pop() - } - - /** - * Returns a port to the pool. - * - * After this call, the port can be reused. Does nothing if the port is not - * managed by the relay. @param {number} port Port - */ - returnPort (port) { - if (!this.#sockets.has(port)) { - return - } - - this.#freePorts.push(port) - } - - /** - * Check if there are any free ports in the pool. @returns {boolean} True if - * there's a free port, false otherwise - */ - hasFreePort () { - return this.#freePorts.length > 0 - } - - /** - * Close all sockets managed by this pool, freeing up all associated system - * resources. - */ - clear () { - [...this.#sockets.keys()].forEach(port => this.deallocatePort(port)) - } - - /** - * Allocated ports. - * @type {number[]} - */ - get ports () { - return [...this.#sockets.keys()] - } -} diff --git a/src/relay/udp.socket.pool.ts b/src/relay/udp.socket.pool.ts new file mode 100644 index 0000000..d6f9959 --- /dev/null +++ b/src/relay/udp.socket.pool.ts @@ -0,0 +1,127 @@ +import assert from "node:assert"; +import dgram from "node:dgram"; + +/** + * Class to manage and allocate UDP ports as needed. + * + * The socket pool is used during UDP relaying. We need to both listen on and + * send traffic through multiple ports during relay. The socket pool ensures + * that these sockets are always available. + * + * This is a low-level interface, concerned only with the allocation, storage + * and deallocation of ports and by extension, UDP sockets. + */ +export class UDPSocketPool { + /** + * Port to socket + * @type {Map} + */ + private sockets = new Map(); + + /** + * Free ports + * @type {number[]} + */ + private freePorts = [] as number[]; + + /** + * Allocate a new port for relaying. + * + * If port is unset or 0, a random port will be picked by the OS. + * @param port Port to allocate + * @returns Allocated port + * @throws if allocation fails + */ + allocatePort(port: number = 0): Promise { + return new Promise((resolve, reject) => { + const socket = dgram.createSocket("udp4"); + socket.once("error", reject); + socket.bind(port ?? 0, () => { + port = this.addSocket(socket); + resolve(port); + }); + }); + } + + /** + * Close the socket associated with the given port. + * + * Does nothing if the port is not managed by the relay. + * @param port Port + */ + deallocatePort(port: number) { + this.sockets.get(port)?.close(); + this.sockets.delete(port); + this.freePorts = this.freePorts.filter((p) => p !== port); + } + + /** + * Get socket listening on port. + * @param port Port + */ + getSocket(port: number): dgram.Socket | undefined { + return this.sockets.get(port); + } + + /** + * Add an already listening socket to use for relaying. + * @param socket Socket + * @returns Relay port + */ + addSocket(socket: dgram.Socket): number { + const port = socket.address().port; + this.sockets.set(port, socket); + this.freePorts.push(port); + + return port; + } + + /** + * Get a free port to use for relaying. + * + * The resulting port can be converted into a socket using `getSocket`. + * @throws if no free ports are available + */ + getPort(): number { + assert(this.freePorts.length > 0, "No more free ports!"); + return this.freePorts.pop()!!; + } + + /** + * Returns a port to the pool. + * + * After this call, the port can be reused. Does nothing if the port is not + * managed by the relay. + */ + returnPort(port: number) { + if (!this.sockets.has(port)) { + return; + } + + this.freePorts.push(port); + } + + /** + * Check if there are any free ports in the pool. + * + * @returns True if there's a free port, false otherwise + */ + hasFreePort(): boolean { + return this.freePorts.length > 0; + } + + /** + * Close all sockets managed by this pool, freeing up all associated system + * resources. + */ + clear() { + [...this.sockets.keys()].forEach((port) => this.deallocatePort(port)); + } + + /** + * Allocated ports. + */ + get ports(): number[] { + return [...this.sockets.keys()]; + } +} diff --git a/src/utils.ts b/src/utils.ts index cae5199..5e01564 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -163,6 +163,32 @@ export function formatByteSize(size: number): string { return size / Math.pow(1024, pfi) + postfixes[pfi]; } +/** + * Format bandwidth in bytes/ec to a human readable form. + * + * E.g. 3072 becomes 3kbps + */ +export function formatBandwidth(bandwidth: number): string { + // TODO: Extract shared logic + const postfixes = [ + "bps", + "kbps", + "Mbps", + "Gbps", + "Tbps", + "Pbps", + "Ebps", + "Zbps", + "Ybps", + ]; + const pfi = + (postfixes.length + + postfixes.findIndex((_, i) => bandwidth < Math.pow(1024, i + 1))) % + postfixes.length; + + return bandwidth / Math.pow(1024, pfi) + postfixes[pfi]; +} + /** * Format a duration to a human readable form. * From 1297b66c99343aaf3c4c5e301b6a40c4e1939927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 23:40:40 +0200 Subject: [PATCH 09/22] fix stuff --- src/relay/relay.ts | 2 +- src/relay/udp.relay.handler.ts | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/relay/relay.ts b/src/relay/relay.ts index 736c5c9..b644ee8 100644 --- a/src/relay/relay.ts +++ b/src/relay/relay.ts @@ -23,7 +23,7 @@ export const udpRelayHandler = new UDPRelayHandler({ export const udpRemoteRegistrar = new UDPRemoteRegistrar({ hostRepository, - udpRelayHandler, + // udpRelayHandler, // TODO: Wtf?? }); const log = logger.child({ name: "mod:relay" }); diff --git a/src/relay/udp.relay.handler.ts b/src/relay/udp.relay.handler.ts index 828d698..57390ba 100644 --- a/src/relay/udp.relay.handler.ts +++ b/src/relay/udp.relay.handler.ts @@ -59,10 +59,16 @@ export class UDPRelayHandler extends EventEmitter { */ public readonly socketPool: UDPSocketPool; + private _relayTable: RelayEntry[] = []; + /** * Relay table used for relaying. */ - public readonly relayTable: RelayEntry[] = []; + public get relayTable(): RelayEntry[] { + // HACK: Let's hope nobody modifies this; kinda don't want to copy it on + // every return + return this._relayTable; + } constructor(options: UDPRelayHandlerOptions) { super(); @@ -83,7 +89,7 @@ export class UDPRelayHandler extends EventEmitter { if (this.hasRelay(relay)) { // We already have this relay entry log.trace({ relay }, "Relay already exists, ignoring"); - return this.relayTable.find((e) => e.equals(relay))!!; + return this._relayTable.find((e) => e.equals(relay))!!; } relay.port = this.socketPool.getPort(); @@ -96,7 +102,7 @@ export class UDPRelayHandler extends EventEmitter { relay.lastReceived = time(); relay.created = time(); - this.relayTable.push(relay); + this._relayTable.push(relay); log.trace({ relay }, "Relay created"); activeRelayGauge.inc(); @@ -110,7 +116,7 @@ export class UDPRelayHandler extends EventEmitter { * NOTE: This only compares the addresses, not the allocated port. */ hasRelay(relay: RelayEntry): boolean { - return this.relayTable.find((e) => e.equals(relay)) !== undefined; + return this._relayTable.find((e) => e.equals(relay)) !== undefined; } /** @@ -118,7 +124,7 @@ export class UDPRelayHandler extends EventEmitter { * @fires UDPRelayHandler#destroy */ freeRelay(relay: RelayEntry): boolean { - const idx = this.relayTable.findIndex((e) => e.equals(relay)); + const idx = this._relayTable.findIndex((e) => e.equals(relay)); if (idx < 0) { return false; } @@ -126,7 +132,7 @@ export class UDPRelayHandler extends EventEmitter { this.emit("destroy", relay); this.socketPool.returnPort(relay.port); - this.relayTable = this.relayTable.filter((_, i) => i !== idx); // TODO: Ugh + this._relayTable = this.relayTable.filter((_, i) => i !== idx); activeRelayGauge.dec(); @@ -137,7 +143,7 @@ export class UDPRelayHandler extends EventEmitter { * Free all relay entries. */ clear() { - this.relayTable.forEach((entry) => this.freeRelay(entry)); + this._relayTable.forEach((entry) => this.freeRelay(entry)); activeRelayGauge.reset(); } @@ -152,11 +158,11 @@ export class UDPRelayHandler extends EventEmitter { relay(msg: Buffer, sender: NetAddress, target: number): boolean { const measure = relayDurationHistogram.startTimer(); - const senderRelay = this.relayTable.find( + const senderRelay = this._relayTable.find( (r) => r.address.port === sender.port && r.address.address === sender.address, ); - const targetRelay = this.relayTable.find((r) => r.port === target); + const targetRelay = this._relayTable.find((r) => r.port === target); if (!senderRelay || !targetRelay) { // We don't have a relay for the sender, target, or both From d36dad4821a7be3073025b8dfc919642c139d33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sat, 6 Jun 2026 23:57:20 +0200 Subject: [PATCH 10/22] src kinda ported? --- bin/noray.mjs | 7 ------- bin/noray.ts | 7 +++++++ package.json | 7 ++++--- src/config.ts | 8 ++++---- src/connection/connection.commands.ts | 10 +++++----- src/connection/connection.ts | 8 ++++---- src/hosts/host.commands.ts | 8 ++++---- src/hosts/host.entity.ts | 8 ++++---- src/hosts/host.repository.ts | 4 ++-- src/hosts/host.ts | 8 ++++---- src/logger.ts | 2 +- src/metrics/metrics.ts | 8 ++++---- src/noray.ts | 16 ++++++++-------- src/relay/bandwidth.limiter.ts | 4 +--- src/relay/constraints.ts | 6 +++--- src/relay/dynamic.relaying.ts | 8 ++++---- src/relay/relay.entry.ts | 4 ++-- src/relay/relay.ts | 27 ++++++++++++--------------- src/relay/udp.relay.cleanup.ts | 8 ++++---- src/relay/udp.relay.handler.ts | 12 ++++++------ src/relay/udp.remote.registrar.ts | 8 ++++---- src/utils.ts | 2 +- 22 files changed, 88 insertions(+), 92 deletions(-) delete mode 100644 bin/noray.mjs create mode 100644 bin/noray.ts diff --git a/bin/noray.mjs b/bin/noray.mjs deleted file mode 100644 index 649a320..0000000 --- a/bin/noray.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { Noray } from '../src/noray.mjs' - -const noray = new Noray() -noray.start() - -process.on('exit', () => noray.shutdown()) -process.on('SIGINT', () => noray.shutdown()) diff --git a/bin/noray.ts b/bin/noray.ts new file mode 100644 index 0000000..53161d8 --- /dev/null +++ b/bin/noray.ts @@ -0,0 +1,7 @@ +import { Noray } from "../src/noray.ts"; + +const noray = new Noray(); +noray.start(); + +process.on("exit", () => noray.shutdown()); +process.on("SIGINT", () => noray.shutdown()); diff --git a/package.json b/package.json index f40acaf..7144c56 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,13 @@ "name": "@foxssake/noray", "version": "1.5.2", "description": "Online multiplayer orchestrator and potential game platform", - "main": "src/noray.mjs", + "main": "src/noray.ts", + "type": "module", "bin": { - "noray": "bin/noray.mjs" + "noray": "bin/noray.ts" }, "scripts": { - "lint": "eslint --ext .mjs src", + "lint": "eslint --ext .ts src", "doc": "jsdoc -c .jsdoc.js src", "test": "node --test test/spec/**/*.test.ts", "test:e2e": "node --test test/e2e/**/*.test.ts | node scripts/taplog.mjs utap \"pino-pretty -c\"", diff --git a/src/config.ts b/src/config.ts index 99ca281..a59b8a4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,8 +6,8 @@ import { integer, number, ports, -} from "./config.parsers"; -import logger, { getLogLevel } from "./logger.js"; +} from "./config.parsers.ts"; +import logger, { getLogLevel } from "./logger.ts"; import { urlAlphabet } from "nanoid"; type ConfigEnv = { [key: string]: string | undefined }; @@ -19,7 +19,7 @@ export function readConfig(env: ConfigEnv) { charset: env.NORAY_OID_CHARSET ?? urlAlphabet, }, - words_oid: { + wordsOid: { enabled: boolean(env.NORAY_ENABLE_WORDS_OID) ?? false, length: integer(env.NORAY_WORDS_OID_LENGTH) ?? 3, }, @@ -77,7 +77,7 @@ export function readLiveConfig() { export type NorayConfig = ReturnType; export type OidConfig = NorayConfig["oid"]; -export type WordsOidConfig = NorayConfig["words_oid"]; +export type WordsOidConfig = NorayConfig["wordsOid"]; export type PidConfig = NorayConfig["pid"]; export type SocketConfig = NorayConfig["socket"]; export type HttpConfig = NorayConfig["http"]; diff --git a/src/connection/connection.commands.ts b/src/connection/connection.commands.ts index a376cf4..b0369ca 100644 --- a/src/connection/connection.commands.ts +++ b/src/connection/connection.commands.ts @@ -1,11 +1,11 @@ -import { HostRepository } from "../hosts/host.repository.js"; +import { HostRepository } from "../hosts/host.repository.ts"; import { NodeSocketReactor } from "@foxssake/trimsock-node"; import { RemoteInfo } from "node:dgram"; import assert from "node:assert"; -import logger from "../logger"; -import { udpRelayHandler } from "../relay/relay.js"; -import { RelayEntry } from "../relay/relay.entry.js"; -import { NetAddress } from "../relay/net.address.js"; +import logger from "../logger.ts"; +import { udpRelayHandler } from "../relay/relay.ts"; +import { RelayEntry } from "../relay/relay.entry.ts"; +import { NetAddress } from "../relay/net.address.ts"; export function handleConnect(hostRepository: HostRepository) { return function(server: NodeSocketReactor) { diff --git a/src/connection/connection.ts b/src/connection/connection.ts index 8a21481..6db45ce 100644 --- a/src/connection/connection.ts +++ b/src/connection/connection.ts @@ -1,7 +1,7 @@ -import { Noray } from "../noray.js"; -import logger from "../logger"; -import { handleConnect, handleConnectRelay } from "./connection.commands"; -import { hostRepository } from "../hosts/host"; +import { Noray } from "../noray.ts"; +import logger from "../logger.ts"; +import { handleConnect, handleConnectRelay } from "./connection.commands.ts"; +import { hostRepository } from "../hosts/host.ts"; const log = logger.child({ name: "mod:connection" }); diff --git a/src/hosts/host.commands.ts b/src/hosts/host.commands.ts index 46bb12a..95038a2 100644 --- a/src/hosts/host.commands.ts +++ b/src/hosts/host.commands.ts @@ -1,9 +1,9 @@ -import { HostRepository } from "./host.repository"; +import { HostRepository } from "./host.repository.ts"; import { NodeSocketReactor } from "@foxssake/trimsock-node"; -import { makeHost } from "./host.entity"; -import logger from "../logger"; +import { makeHost } from "./host.entity.ts"; +import logger from "../logger.ts"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry.js"; +import { metricsRegistry } from "../metrics/metrics.registry.ts"; const activeHostsGauge = new prometheus.Gauge({ name: "noray_active_hosts", diff --git a/src/hosts/host.entity.ts b/src/hosts/host.entity.ts index ef57552..1d13844 100644 --- a/src/hosts/host.entity.ts +++ b/src/hosts/host.entity.ts @@ -1,11 +1,11 @@ import * as net from "node:net"; import * as dgram from "node:dgram"; import * as nanoid from "nanoid"; -import { config } from "../config"; -import { generateWordId } from "../utils"; +import { config } from "../config.ts"; +import { generateWordId } from "../utils.ts"; -const generateOID = config.words_oid.enabled - ? () => generateWordId(config.words_oid.length) +const generateOID = config.wordsOid.enabled + ? () => generateWordId(config.wordsOid.length) : nanoid.customAlphabet(config.oid.charset, config.oid.length); const generatePID = nanoid.customAlphabet( config.pid.charset, diff --git a/src/hosts/host.repository.ts b/src/hosts/host.repository.ts index 7884af3..ae445b7 100644 --- a/src/hosts/host.repository.ts +++ b/src/hosts/host.repository.ts @@ -1,6 +1,6 @@ import * as net from "node:net"; -import { HostEntity } from "./host.entity"; -import { Repository } from "../repository"; +import { HostEntity } from "./host.entity.ts"; +import { Repository } from "../repository.ts"; /** * Repository for tracking hosts. diff --git a/src/hosts/host.ts b/src/hosts/host.ts index 8224451..413ccde 100644 --- a/src/hosts/host.ts +++ b/src/hosts/host.ts @@ -1,7 +1,7 @@ -import { Noray } from "../noray.js"; -import logger from "../logger"; -import { handleRegisterHost } from "./host.commands"; -import { HostRepository } from "./host.repository"; +import { Noray } from "../noray.ts"; +import logger from "../logger.ts"; +import { handleRegisterHost } from "./host.commands.ts"; +import { HostRepository } from "./host.repository.ts"; const log = logger.child({ name: "mod:host" }); diff --git a/src/logger.ts b/src/logger.ts index 3eb003e..de46d10 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,6 @@ import pino from "pino"; import * as dotenv from "dotenv"; -import { enumerated } from "./config.parsers"; +import { enumerated } from "./config.parsers.ts"; export const loglevels = Object.freeze([ "silent", diff --git a/src/metrics/metrics.ts b/src/metrics/metrics.ts index d1b366e..3eaa671 100644 --- a/src/metrics/metrics.ts +++ b/src/metrics/metrics.ts @@ -1,9 +1,9 @@ import * as http from "node:http"; -import { Noray } from "../noray.js"; -import logger from "../logger"; +import { Noray } from "../noray.ts"; +import logger from "../logger.ts"; import * as prometheus from "prom-client"; -import { config } from "../config"; -import { metricsRegistry } from "./metrics.registry"; +import { config } from "../config.ts"; +import { metricsRegistry } from "./metrics.registry.ts"; const log = logger.child({ name: "mod:metrics" }); diff --git a/src/noray.ts b/src/noray.ts index dff782c..d3fd0c2 100644 --- a/src/noray.ts +++ b/src/noray.ts @@ -1,17 +1,17 @@ import * as net from "node:net"; import { EventEmitter } from "node:events"; -import logger from "./logger"; -import { config } from "./config"; +import logger from "./logger.ts"; +import { config } from "./config.ts"; import { NodeSocketReactor } from "@foxssake/trimsock-node"; -import { promiseEvent } from "./utils"; +import { promiseEvent } from "./utils.ts"; export type NorayHook = (noray: Noray) => void; const defaultModules = [ - "metrics/metrics.mjs", - "relay/relay.mjs", - "hosts/host.mjs", - "connection/connection.mjs", + "metrics/metrics.ts", + "relay/relay.ts", + "hosts/host.ts", + "connection/connection.ts", ]; export class Noray extends EventEmitter { @@ -28,7 +28,7 @@ export class Noray extends EventEmitter { this.hooks.push(hok); } - async start(modules: string[]): Promise { + async start(modules: string[] = defaultModules): Promise { modules ??= defaultModules; this.log.info("Starting Noray"); diff --git a/src/relay/bandwidth.limiter.ts b/src/relay/bandwidth.limiter.ts index deecdac..1c9a668 100644 --- a/src/relay/bandwidth.limiter.ts +++ b/src/relay/bandwidth.limiter.ts @@ -1,6 +1,4 @@ -import assert from "node:assert"; -import { requireParam } from "../assertions"; -import { formatBandwidth, time } from "../utils"; +import { formatBandwidth, time } from "../utils.ts"; /** * Constructor options for [BandwidthLimiter] diff --git a/src/relay/constraints.ts b/src/relay/constraints.ts index 35fa712..8c875c0 100644 --- a/src/relay/constraints.ts +++ b/src/relay/constraints.ts @@ -1,7 +1,7 @@ -import { BandwidthLimiter } from "./bandwidth.limiter"; -import { UDPRelayHandler } from "./udp.relay.handler.js"; +import { BandwidthLimiter } from "./bandwidth.limiter.ts"; +import { UDPRelayHandler } from "./udp.relay.handler.ts"; import assert from "node:assert"; -import { time } from "../utils"; +import { time } from "../utils.ts"; /** * Limit the bandwidth on every relay individually. diff --git a/src/relay/dynamic.relaying.ts b/src/relay/dynamic.relaying.ts index 85cea4c..62540aa 100644 --- a/src/relay/dynamic.relaying.ts +++ b/src/relay/dynamic.relaying.ts @@ -1,7 +1,7 @@ -import { NetAddress } from "./net.address"; -import { UDPRelayHandler } from "./udp.relay.handler.js"; -import logger from "../logger"; -import { RelayEntry } from "./relay.entry.js"; +import { NetAddress } from "./net.address.ts"; +import { UDPRelayHandler } from "./udp.relay.handler.ts"; +import logger from "../logger.ts"; +import { RelayEntry } from "./relay.entry.ts"; const log = logger.child({ name: "DynamicRelaying" }); diff --git a/src/relay/relay.entry.ts b/src/relay/relay.entry.ts index 614f4e1..eba8185 100644 --- a/src/relay/relay.entry.ts +++ b/src/relay/relay.entry.ts @@ -1,5 +1,5 @@ -import { NetAddress } from "./net.address"; -import { time } from "../utils"; +import { NetAddress } from "./net.address.ts"; +import { time } from "../utils.ts"; export interface RelayEntryData { /** diff --git a/src/relay/relay.ts b/src/relay/relay.ts index b644ee8..ed255f0 100644 --- a/src/relay/relay.ts +++ b/src/relay/relay.ts @@ -1,30 +1,27 @@ -import { config } from "../config"; +import { config } from "../config.ts"; import { constrainGlobalBandwidth, constrainIndividualBandwidth, constrainLifetime, constrainTraffic, -} from "./constraints"; -import { UDPRelayHandler } from "./udp.relay.handler.js"; -import { Noray } from "../noray.js"; -import { cleanupUdpRelayTable } from "./udp.relay.cleanup.js"; -import logger from "../logger"; -import { formatByteSize, formatDuration } from "../utils"; -import { UDPRemoteRegistrar } from "./udp.remote.registrar.js"; -import { hostRepository } from "../hosts/host.js"; -import { useDynamicRelay } from "./dynamic.relaying.js"; -import { UDPSocketPool } from "./udp.socket.pool.js"; +} from "./constraints.ts"; +import { UDPRelayHandler } from "./udp.relay.handler.ts"; +import { Noray } from "../noray.ts"; +import { cleanupUdpRelayTable } from "./udp.relay.cleanup.ts"; +import logger from "../logger.ts"; +import { formatByteSize, formatDuration } from "../utils.ts"; +import { UDPRemoteRegistrar } from "./udp.remote.registrar.ts"; +import { hostRepository } from "../hosts/host.ts"; +import { useDynamicRelay } from "./dynamic.relaying.ts"; +import { UDPSocketPool } from "./udp.socket.pool.ts"; export const udpSocketPool = new UDPSocketPool(); export const udpRelayHandler = new UDPRelayHandler({ socketPool: udpSocketPool, }); +export const udpRemoteRegistrar = new UDPRemoteRegistrar({ hostRepository }); -export const udpRemoteRegistrar = new UDPRemoteRegistrar({ - hostRepository, - // udpRelayHandler, // TODO: Wtf?? -}); const log = logger.child({ name: "mod:relay" }); Noray.hook(async (noray) => { diff --git a/src/relay/udp.relay.cleanup.ts b/src/relay/udp.relay.cleanup.ts index 9498a64..d31432e 100644 --- a/src/relay/udp.relay.cleanup.ts +++ b/src/relay/udp.relay.cleanup.ts @@ -1,8 +1,8 @@ -import { UDPRelayHandler } from "./udp.relay.handler.js"; -import { time } from "../utils"; +import { UDPRelayHandler } from "./udp.relay.handler.ts"; +import { time } from "../utils.ts"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry"; -import { RelayEntry } from "./relay.entry"; +import { metricsRegistry } from "../metrics/metrics.registry.ts"; +import { RelayEntry } from "./relay.entry.ts"; const expiredRelayCounter = new prometheus.Counter({ name: "noray_relay_expired", diff --git a/src/relay/udp.relay.handler.ts b/src/relay/udp.relay.handler.ts index 57390ba..84b2c49 100644 --- a/src/relay/udp.relay.handler.ts +++ b/src/relay/udp.relay.handler.ts @@ -1,11 +1,11 @@ -import { RelayEntry } from "./relay.entry"; -import { NetAddress } from "./net.address"; -import { UDPSocketPool } from "./udp.socket.pool.js"; -import { time } from "../utils"; +import { RelayEntry } from "./relay.entry.ts"; +import { NetAddress } from "./net.address.ts"; +import { UDPSocketPool } from "./udp.socket.pool.ts"; +import { time } from "../utils.ts"; import { EventEmitter } from "node:events"; -import logger from "../logger"; +import logger from "../logger.ts"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry"; +import { metricsRegistry } from "../metrics/metrics.registry.ts"; const log = logger.child({ name: "UDPRelayHandler" }); diff --git a/src/relay/udp.remote.registrar.ts b/src/relay/udp.remote.registrar.ts index 54bd533..df58cda 100644 --- a/src/relay/udp.remote.registrar.ts +++ b/src/relay/udp.remote.registrar.ts @@ -1,10 +1,10 @@ -import { HostRepository } from "../hosts/host.repository"; +import { HostRepository } from "../hosts/host.repository.ts"; import dgram from "node:dgram"; import assert from "node:assert"; -import logger from "../logger"; -import { requireParam } from "../assertions"; +import logger from "../logger.ts"; +import { requireParam } from "../assertions.ts"; import * as prometheus from "prom-client"; -import { metricsRegistry } from "../metrics/metrics.registry"; +import { metricsRegistry } from "../metrics/metrics.registry.ts"; const log = logger.child({ name: "UDPRemoteRegistrar" }); diff --git a/src/utils.ts b/src/utils.ts index 5e01564..d889529 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { randomInt } from "node:crypto"; import { type EventEmitter } from "node:events"; -import words from "./wordlist"; +import words from "./wordlist.ts"; /** * Return current time ( seconds since epoch ). From 6f7c0eab093805be0511d18bfbb15e3cfa2d9c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 01:54:45 +0200 Subject: [PATCH 11/22] fix syntax not supported on node --- src/connection/connection.commands.ts | 2 +- src/hosts/host.repository.ts | 2 +- src/repository.ts | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/connection/connection.commands.ts b/src/connection/connection.commands.ts index b0369ca..1b34575 100644 --- a/src/connection/connection.commands.ts +++ b/src/connection/connection.commands.ts @@ -1,6 +1,6 @@ import { HostRepository } from "../hosts/host.repository.ts"; import { NodeSocketReactor } from "@foxssake/trimsock-node"; -import { RemoteInfo } from "node:dgram"; +import { type RemoteInfo } from "node:dgram"; import assert from "node:assert"; import logger from "../logger.ts"; import { udpRelayHandler } from "../relay/relay.ts"; diff --git a/src/hosts/host.repository.ts b/src/hosts/host.repository.ts index ae445b7..70d3f2c 100644 --- a/src/hosts/host.repository.ts +++ b/src/hosts/host.repository.ts @@ -1,6 +1,6 @@ import * as net from "node:net"; -import { HostEntity } from "./host.entity.ts"; import { Repository } from "../repository.ts"; +import { type HostEntity } from "./host.entity.ts"; /** * Repository for tracking hosts. diff --git a/src/repository.ts b/src/repository.ts index 9a221f7..f85b6d4 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -11,11 +11,17 @@ export class UnknownItemError extends Error { } */ export class Repository { protected items = new Map(); + private getId: IdMapper; + private merge: ItemMerger; constructor( - private getId: IdMapper, - private merge: ItemMerger = (a, b) => ({ ...a, ...b }), - ) { } + // TODO: node doesn't support `private getId: ...` syntax... + getId: IdMapper, + merge: ItemMerger = (a, b) => ({ ...a, ...b }), + ) { + this.getId = getId; + this.merge = merge; + } /** * Add item to repository. From fc1b3840dbfd955cdb27b7dddec88a584de75375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 02:02:42 +0200 Subject: [PATCH 12/22] fxs --- src/relay/relay.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/relay/relay.ts b/src/relay/relay.ts index ed255f0..d0ae6c4 100644 --- a/src/relay/relay.ts +++ b/src/relay/relay.ts @@ -38,7 +38,15 @@ Noray.hook(async (noray) => { "Listening on port %d for UDP remote registrars", config.udpRelay.registrarPort, ); - udpRemoteRegistrar.listen(config.udpRelay.registrarPort); + udpRemoteRegistrar + .listen(config.udpRelay.registrarPort) + .then(() => + log.info( + "Remote registrar started listening on port %d", + config.udpRelay.registrarPort, + ), + ) + .catch((e) => log.error(e, "Remote registrar failed to listen!")); log.info("Binding %d ports for relaying", config.udpRelay.ports!!.length); @@ -86,7 +94,13 @@ Noray.hook(async (noray) => { clearInterval(cleanupJob); log.info("Closing UDP remote registrar socket"); - udpRemoteRegistrar.socket.close(); + try { + // HACK: Removing the try-catch guard results in a "not running" exception? + // On node v24.16.0 + udpRemoteRegistrar.socket.close(); + } catch (e) { + log.warn(e, "Failed to close UDP Remote Registrar socket"); + } log.info("Closing socket pool"); udpSocketPool.clear(); From 22d866f62aeebbbf3ef699b27c35d423c51db128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 11:50:08 +0200 Subject: [PATCH 13/22] port specs --- src/assertions.ts | 18 +- src/relay/dynamic.relaying.ts | 2 +- src/relay/relay.entry.ts | 24 +- src/relay/udp.remote.registrar.ts | 3 +- src/repository.ts | 26 -- src/utils.ts | 4 +- test/spec/assertions.test.mjs | 68 ---- test/spec/assertions.test.ts | 35 ++ test/spec/config.parsers.test.mjs | 145 ------- test/spec/config.parsers.test.ts | 173 +++++++++ test/spec/relay/bandwidth.limiter.test.mjs | 35 -- test/spec/relay/bandwidth.limiter.test.ts | 35 ++ test/spec/relay/constraints.test.mjs | 259 ------------- test/spec/relay/constraints.test.ts | 264 +++++++++++++ test/spec/relay/dynamic.relaying.test.mjs | 130 ------- test/spec/relay/dynamic.relaying.test.ts | 159 ++++++++ test/spec/relay/relay.entry.test.mjs | 39 -- test/spec/relay/relay.entry.test.ts | 65 ++++ test/spec/relay/udp.relay.cleanup.test.mjs | 62 --- test/spec/relay/udp.relay.cleanup.test.ts | 81 ++++ test/spec/relay/udp.relay.handler.test.mjs | 328 ---------------- test/spec/relay/udp.relay.handler.test.ts | 364 ++++++++++++++++++ test/spec/relay/udp.remote.registrar.test.mjs | 108 ------ test/spec/relay/udp.remote.registrar.test.ts | 109 ++++++ test/spec/relay/udp.socket.pool.test.mjs | 134 ------- test/spec/relay/udp.socket.pool.test.ts | 134 +++++++ test/spec/repository.test.mjs | 231 ----------- test/spec/repository.test.ts | 236 ++++++++++++ test/spec/utils.test.mjs | 192 --------- test/spec/utils.test.ts | 221 +++++++++++ 30 files changed, 1910 insertions(+), 1774 deletions(-) delete mode 100644 test/spec/assertions.test.mjs create mode 100644 test/spec/assertions.test.ts delete mode 100644 test/spec/config.parsers.test.mjs create mode 100644 test/spec/config.parsers.test.ts delete mode 100644 test/spec/relay/bandwidth.limiter.test.mjs create mode 100644 test/spec/relay/bandwidth.limiter.test.ts delete mode 100644 test/spec/relay/constraints.test.mjs create mode 100644 test/spec/relay/constraints.test.ts delete mode 100644 test/spec/relay/dynamic.relaying.test.mjs create mode 100644 test/spec/relay/dynamic.relaying.test.ts delete mode 100644 test/spec/relay/relay.entry.test.mjs create mode 100644 test/spec/relay/relay.entry.test.ts delete mode 100644 test/spec/relay/udp.relay.cleanup.test.mjs create mode 100644 test/spec/relay/udp.relay.cleanup.test.ts delete mode 100644 test/spec/relay/udp.relay.handler.test.mjs create mode 100644 test/spec/relay/udp.relay.handler.test.ts delete mode 100644 test/spec/relay/udp.remote.registrar.test.mjs create mode 100644 test/spec/relay/udp.remote.registrar.test.ts delete mode 100644 test/spec/relay/udp.socket.pool.test.mjs create mode 100644 test/spec/relay/udp.socket.pool.test.ts delete mode 100644 test/spec/repository.test.mjs create mode 100644 test/spec/repository.test.ts delete mode 100644 test/spec/utils.test.mjs create mode 100644 test/spec/utils.test.ts diff --git a/src/assertions.ts b/src/assertions.ts index 39bb24b..251ec97 100644 --- a/src/assertions.ts +++ b/src/assertions.ts @@ -1,14 +1,5 @@ import { fail } from "node:assert"; -/** - * Ensure param is present. - * - * @throws on missing value - */ -export function requireParam(value: any) { - return value ?? fail("Mising parameter!"); -} - /** * Ensure param is a valid enum value. * @@ -16,7 +7,14 @@ export function requireParam(value: any) { * * @throws on invalid `value` */ -export function requireEnum(value: T, enumDef: Record) { +export function requireEnum( + value: T, + enumDef: Readonly>, +): T; +export function requireEnum( + value: T, + enumDef: Record, +): T { return Object.values(enumDef).includes(value) ? value : fail("Invalid enum value: " + value); diff --git a/src/relay/dynamic.relaying.ts b/src/relay/dynamic.relaying.ts index 62540aa..59c99a1 100644 --- a/src/relay/dynamic.relaying.ts +++ b/src/relay/dynamic.relaying.ts @@ -75,7 +75,7 @@ export class DynamicRelaying { address: senderAddress, port, }); - await relayHandler.createRelay(relay); + relayHandler.createRelay(relay); log.info( { relay }, diff --git a/src/relay/relay.entry.ts b/src/relay/relay.entry.ts index eba8185..11547cf 100644 --- a/src/relay/relay.entry.ts +++ b/src/relay/relay.entry.ts @@ -11,6 +11,21 @@ export interface RelayEntryData { * The target address where traffic should be forwarded */ address: NetAddress; + + /** + * Time the relay was last used to send data. + */ + lastSent?: number; + + /** + * Time the relay last received traffic on its port. + */ + lastReceived?: number; + + /** + * Time the relay was created. + */ + created?: number; } /** @@ -30,17 +45,17 @@ export class RelayEntry implements RelayEntryData { /** * Time the relay was last used to send data. */ - lastSent = 0; + lastSent: number; /** * Time the relay last received traffic on its port. */ - lastReceived = 0; + lastReceived: number; /** * Time the relay was created. */ - created = time(); + created: number; /** * Construct entry @@ -48,6 +63,9 @@ export class RelayEntry implements RelayEntryData { constructor(options: RelayEntryData) { this.port = options.port; this.address = options.address; + this.lastSent = options.lastSent ?? 0; + this.lastReceived = options.lastReceived ?? 0; + this.created = options.created ?? time(); } /** diff --git a/src/relay/udp.remote.registrar.ts b/src/relay/udp.remote.registrar.ts index df58cda..23c3b43 100644 --- a/src/relay/udp.remote.registrar.ts +++ b/src/relay/udp.remote.registrar.ts @@ -2,7 +2,6 @@ import { HostRepository } from "../hosts/host.repository.ts"; import dgram from "node:dgram"; import assert from "node:assert"; import logger from "../logger.ts"; -import { requireParam } from "../assertions.ts"; import * as prometheus from "prom-client"; import { metricsRegistry } from "../metrics/metrics.registry.ts"; @@ -51,7 +50,7 @@ export class UDPRemoteRegistrar { private hostRepository: HostRepository; constructor(options: UDPRemoteRegistrarOptions) { - this.hostRepository = requireParam(options.hostRepository); + this.hostRepository = options.hostRepository; this.socket = options.socket ?? dgram.createSocket("udp4"); } diff --git a/src/repository.ts b/src/repository.ts index f85b6d4..6edf449 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -126,29 +126,3 @@ export class Repository { return id; } } - -/** - * Create an id mapper that grabs the given field of the object. - */ -export function fieldIdMapper, K>( - field: string, -): IdMapper { - return (v) => v[field]; -} - -/** - * Create an id mapper that grabs the `id` field of the object. - */ -export function idFieldMapper(): IdMapper { - return (v) => v.id; -} - -/** - * Create an item merger that uses assignment update the object without changing - * the reference. - */ -export function assignMerger(): ItemMerger { - return (current, update) => { - return { current, ...update } as T; - }; -} diff --git a/src/utils.ts b/src/utils.ts index d889529..eb68a4d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,7 +12,9 @@ export function time(): number { /** * Sleep. */ -export function sleep(seconds: number, value: T): Promise { +export function sleep(seconds: number): Promise; +export function sleep(seconds: number, value: T): Promise; +export function sleep(seconds: number, value?: T): Promise { return new Promise((resolve) => setTimeout(() => resolve(value), seconds * 1000), ); diff --git a/test/spec/assertions.test.mjs b/test/spec/assertions.test.mjs deleted file mode 100644 index 5b05142..0000000 --- a/test/spec/assertions.test.mjs +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import { requireEnum, requireParam } from '../../src/assertions.mjs' - -describe('assertions', () => { - describe('requireParam', () => { - it('should return param', () => { - // Given - const expected = 42 - - // When - const actual = requireParam(expected) - - // Then - assert.equal(actual, expected) - }) - - it('should throw on missing', () => { - // Given - const param = undefined - - // When + Then - assert.throws( - () => requireParam(param) - ) - }) - - it('should not throw on falsy', () => { - // Given - const expected = false - - // When - const actual = requireParam(expected) - - // Then - assert.equal(actual, expected) - }) - }) - - describe('requireEnum', () => { - const TestEnum = Object.freeze({ - Foo: 'foo', - Bar: 'bar' - }) - - it('should return param', () => { - // Given - const expected = TestEnum.Bar - - // When - const actual = requireEnum(expected, TestEnum) - - // Then - assert.equal(actual, expected) - }) - - it('should throw on invalid', () => { - // Given - const invalid = '@@$invalid$@@' - - // When + Then - assert.throws( - () => requireEnum(invalid, TestEnum), - err => assert.equal(err.message, 'Invalid enum value: ' + invalid) ?? true - ) - }) - }) -}) diff --git a/test/spec/assertions.test.ts b/test/spec/assertions.test.ts new file mode 100644 index 0000000..fc842da --- /dev/null +++ b/test/spec/assertions.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { requireEnum } from "../../src/assertions.ts"; + +describe("assertions", () => { + describe("requireEnum", () => { + const TestEnum = Object.freeze({ + Foo: "foo", + Bar: "bar", + }); + + it("should return param", () => { + // Given + const expected = TestEnum.Bar; + + // When + const actual = requireEnum(expected, TestEnum); + + // Then + assert.equal(actual, expected); + }); + + it("should throw on invalid", () => { + // Given + const invalid = "@@$invalid$@@"; + + // When + Then + assert.throws( + () => requireEnum(invalid, TestEnum), + (err: Error) => + assert.equal(err.message, "Invalid enum value: " + invalid) ?? true, + ); + }); + }); +}); diff --git a/test/spec/config.parsers.test.mjs b/test/spec/config.parsers.test.mjs deleted file mode 100644 index 3aa1547..0000000 --- a/test/spec/config.parsers.test.mjs +++ /dev/null @@ -1,145 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import { boolean, byteSize, duration, enumerated, integer, number, ports } from '../../src/config.parsers.mjs' - -function Case(name, input, expected) { - return { name, input, expected } -} - -const cases = { - boolean: { - method: value => boolean(value), - cases: [ - Case('should parse true', 'true', true), - Case('should parse false', 'false', false), - Case('should parse any word', 'foo', false), - Case('should return undefined on undefined', undefined, undefined) - ] - }, - - integer: { - method: value => integer(value), - cases: [ - Case('should parse valid', '42', 42), - Case('should return undefined on invalid', 'asd', undefined), - Case('should return undefined on empty', '', undefined), - Case('should return undefined on undefined', undefined, undefined) - ] - }, - - number: { - method: value => number(value), - cases: [ - Case('should parse valid integer', '42', 42), - Case('should parse valid number', '420.69', 420.69), - Case('should return undefined on invalid', 'asd', undefined), - Case('should return undefined on empty', '', undefined), - Case('should return undefined on undefined', undefined, undefined) - ] - }, - - enumerated: { - method: ([value, known]) => enumerated(value, known), - cases: [ - Case('should return known', ['a', ['a', 'b', 'c']], 'a'), - Case('should return undefined on unknown', ['f', ['a', 'b']], undefined), - Case('should return undefined on empty', ['', ['a', 'b']], undefined), - Case('should return undefined on undefined', [undefined, ['a']], undefined) - ] - } -} - -Object.entries(cases).forEach(([name, entry]) => { - describe(name, () => { - entry.cases.forEach(kase => { - it(kase.name, () => { - // Given - const expected = kase.expected - - // When - const actual = entry.method(kase.input) - - // Then - assert.deepEqual(actual, expected) - }) - }) - }) -}) - -describe('byteSize', () => { - const validCases = [ - Case('should pass through undefined', undefined, undefined), - Case('should parse without postfix', '64', 64), - Case('should parse kb', '64kb', 64 * 1024), - Case('should parse Mb', '64Mb', 64 * Math.pow(1024, 2)), - Case('should parse Gb', '64Gb', 64 * Math.pow(1024, 3)), - Case('should parse Gb', '64Tb', 64 * Math.pow(1024, 4)), - Case('should parse Pb', '6.4Pb', 6.4 * Math.pow(1024, 5)), - Case('should parse Eb', '6.4Eb', 6.4 * Math.pow(1024, 6)), - Case('should parse Zb', '64Zb', 64 * Math.pow(1024, 7)), - Case('should parse Yb', '64Yb', 64 * Math.pow(1024, 8)), - ] - - const throwCases = [ - Case('should throw on invalid format', 'no6'), - Case('should throw on invalid postfix', '64Bb') - ] - - validCases.forEach(kase => - it(kase.name, () => assert.equal(byteSize(kase.input), kase.expected)) - ) - - throwCases.forEach(kase => - it(kase.name, () => assert.throws(() => - byteSize(kase.input) - )) - ) -}) - -describe('duration', () => { - const validCases = [ - Case('should pass through undefined', undefined, undefined), - Case('should parse without postfix', '64', 64), - Case('should parse usec', '64us', 0.000064), - Case('should parse msec', '64ms', 0.064), - Case('should parse sec', '64s', 64), - Case('should parse minute', '10m', 600), - Case('should parse hour', '4h', 14400), - Case('should parse hour', '4hr', 14400), - Case('should parse day', '2d', 172800), - Case('should parse week', '2w', 1209600), - Case('should parse month', '3mo', 7776000), - Case('should parse year', '4yr', 126144000), - ] - - const throwCases = [ - Case('should throw on invalid format', 'no6'), - Case('should throw on invalid postfix', '64mh') - ] - - validCases.forEach(kase => - it(kase.name, () => assert.equal(duration(kase.input), kase.expected)) - ) - - throwCases.forEach(kase => - it(kase.name, () => assert.throws(() => - duration(kase.input) - )) - ) -}) - -describe('ports', () => { - const cases = [ - Case('should parse literal', '1024', [1024]), - Case('should parse absolute', '1024-1026', [1024, 1025, 1026]), - Case('should parse relative', '2048+3', [2048, 2049, 2050, 2051]), - Case('should parse single absolute', '1024-1024', [1024]), - Case('should parse single relative', '1024+0', [1024]), - Case('should return sorted', '2048+1, 1024-1025', [1024, 1025, 2048, 2049]), - Case('should return unique', '1-4, 2, 2-6', [1, 2, 3, 4, 5, 6]) - ] - - cases.forEach(kase => - it(kase.name, () => assert.deepEqual(ports(kase.input), kase.expected)) - ) -}) diff --git a/test/spec/config.parsers.test.ts b/test/spec/config.parsers.test.ts new file mode 100644 index 0000000..2e05ae8 --- /dev/null +++ b/test/spec/config.parsers.test.ts @@ -0,0 +1,173 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { + boolean, + byteSize, + duration, + enumerated, + integer, + number, + ports, +} from "../../src/config.parsers.ts"; + +interface CaseDef { + name: string; + input: string | [string | undefined, string[]] | undefined; + expected: any | undefined; +} + +function Case( + name: string, + input: CaseDef["input"], + expected?: any | undefined, +): CaseDef { + return { name, input, expected }; +} + +const cases = { + boolean: { + method: (value) => boolean(value), + cases: [ + Case("should parse true", "true", true), + Case("should parse false", "false", false), + Case("should parse any word", "foo", false), + Case("should return undefined on undefined", undefined, undefined), + ], + }, + + integer: { + method: (value) => integer(value), + cases: [ + Case("should parse valid", "42", 42), + Case("should return undefined on invalid", "asd", undefined), + Case("should return undefined on empty", "", undefined), + Case("should return undefined on undefined", undefined, undefined), + ], + }, + + number: { + method: (value) => number(value), + cases: [ + Case("should parse valid integer", "42", 42), + Case("should parse valid number", "420.69", 420.69), + Case("should return undefined on invalid", "asd", undefined), + Case("should return undefined on empty", "", undefined), + Case("should return undefined on undefined", undefined, undefined), + ], + }, + + enumerated: { + method: ([value, known]) => enumerated(value, known), + cases: [ + Case("should return known", ["a", ["a", "b", "c"]], "a"), + Case("should return undefined on unknown", ["f", ["a", "b"]], undefined), + Case("should return undefined on empty", ["", ["a", "b"]], undefined), + Case( + "should return undefined on undefined", + [undefined, ["a"]], + undefined, + ), + ], + }, +} as Record void; cases: CaseDef[] }>; + +Object.entries(cases).forEach(([name, entry]) => { + describe(name, () => { + entry.cases.forEach((kase) => { + it(kase.name, () => { + // Given + const expected = kase.expected; + + // When + const actual = entry.method(kase.input); + + // Then + assert.deepEqual(actual, expected); + }); + }); + }); +}); + +describe("byteSize", () => { + const validCases = [ + Case("should pass through undefined", undefined, undefined), + Case("should parse without postfix", "64", 64), + Case("should parse kb", "64kb", 64 * 1024), + Case("should parse Mb", "64Mb", 64 * Math.pow(1024, 2)), + Case("should parse Gb", "64Gb", 64 * Math.pow(1024, 3)), + Case("should parse Gb", "64Tb", 64 * Math.pow(1024, 4)), + Case("should parse Pb", "6.4Pb", 6.4 * Math.pow(1024, 5)), + Case("should parse Eb", "6.4Eb", 6.4 * Math.pow(1024, 6)), + Case("should parse Zb", "64Zb", 64 * Math.pow(1024, 7)), + Case("should parse Yb", "64Yb", 64 * Math.pow(1024, 8)), + ]; + + const throwCases = [ + Case("should throw on invalid format", "no6"), + Case("should throw on invalid postfix", "64Bb"), + ]; + + validCases.forEach((kase) => + it(kase.name, () => + assert.equal(byteSize(kase.input as string | undefined), kase.expected), + ), + ); + + throwCases.forEach((kase) => + it(kase.name, () => + assert.throws(() => byteSize(kase.input as string | undefined)), + ), + ); +}); + +describe("duration", () => { + const validCases = [ + Case("should pass through undefined", undefined, undefined), + Case("should parse without postfix", "64", 64), + Case("should parse usec", "64us", 0.000064), + Case("should parse msec", "64ms", 0.064), + Case("should parse sec", "64s", 64), + Case("should parse minute", "10m", 600), + Case("should parse hour", "4h", 14400), + Case("should parse hour", "4hr", 14400), + Case("should parse day", "2d", 172800), + Case("should parse week", "2w", 1209600), + Case("should parse month", "3mo", 7776000), + Case("should parse year", "4yr", 126144000), + ]; + + const throwCases = [ + Case("should throw on invalid format", "no6"), + Case("should throw on invalid postfix", "64mh"), + ]; + + validCases.forEach((kase) => + it(kase.name, () => + assert.equal(duration(kase.input as string | undefined), kase.expected), + ), + ); + + throwCases.forEach((kase) => + it(kase.name, () => + assert.throws(() => duration(kase.input as string | undefined)), + ), + ); +}); + +describe("ports", () => { + const cases = [ + Case("should parse literal", "1024", [1024]), + Case("should parse absolute", "1024-1026", [1024, 1025, 1026]), + Case("should parse relative", "2048+3", [2048, 2049, 2050, 2051]), + Case("should parse single absolute", "1024-1024", [1024]), + Case("should parse single relative", "1024+0", [1024]), + Case("should return sorted", "2048+1, 1024-1025", [1024, 1025, 2048, 2049]), + Case("should return unique", "1-4, 2, 2-6", [1, 2, 3, 4, 5, 6]), + ]; + + cases.forEach((kase) => + it(kase.name, () => + assert.deepEqual(ports(kase.input as string | undefined), kase.expected), + ), + ); +}); diff --git a/test/spec/relay/bandwidth.limiter.test.mjs b/test/spec/relay/bandwidth.limiter.test.mjs deleted file mode 100644 index 2e2d286..0000000 --- a/test/spec/relay/bandwidth.limiter.test.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import { BandwidthLimiter } from '../../../src/relay/bandwidth.limiter.mjs' -import { sleep } from '../../../src/utils.mjs' - -describe('BandwidthLimiter', () => { - it('should pass', () => { - // Given - const limiter = new BandwidthLimiter({ maxTraffic: 16 }) - limiter.validate(8) - - // When + Then - assert.doesNotThrow(() => limiter.validate(8)) - }) - - it('should pass after interval', async () => { - // Given - const limiter = new BandwidthLimiter({ maxTraffic: 160, interval: 0.1 }) - limiter.validate(16) - - await sleep(0.15) - - // When + Then - assert.doesNotThrow(() => limiter.validate(8)) - }) - - it('should throw', () => { - // Given - const limiter = new BandwidthLimiter({ maxTraffic: 16 }) - limiter.validate(12) - - // When + Then - assert.throws(() => limiter.validate(8)) - }) -}) diff --git a/test/spec/relay/bandwidth.limiter.test.ts b/test/spec/relay/bandwidth.limiter.test.ts new file mode 100644 index 0000000..fcc86ac --- /dev/null +++ b/test/spec/relay/bandwidth.limiter.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { BandwidthLimiter } from "../../../src/relay/bandwidth.limiter.ts"; +import { sleep } from "../../../src/utils.ts"; + +describe("BandwidthLimiter", () => { + it("should pass", () => { + // Given + const limiter = new BandwidthLimiter({ maxTraffic: 16 }); + limiter.validate(8); + + // When + Then + assert.doesNotThrow(() => limiter.validate(8)); + }); + + it("should pass after interval", async () => { + // Given + const limiter = new BandwidthLimiter({ maxTraffic: 160, interval: 0.1 }); + limiter.validate(16); + + await sleep(0.15); + + // When + Then + assert.doesNotThrow(() => limiter.validate(8)); + }); + + it("should throw", () => { + // Given + const limiter = new BandwidthLimiter({ maxTraffic: 16 }); + limiter.validate(12); + + // When + Then + assert.throws(() => limiter.validate(8)); + }); +}); diff --git a/test/spec/relay/constraints.test.mjs b/test/spec/relay/constraints.test.mjs deleted file mode 100644 index 200cc73..0000000 --- a/test/spec/relay/constraints.test.mjs +++ /dev/null @@ -1,259 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import sinon from 'sinon' -import { RelayEntry } from '../../../src/relay/relay.entry.mjs' -import { NetAddress } from '../../../src/relay/net.address.mjs' -import { UDPRelayHandler } from '../../../src/relay/udp.relay.handler.mjs' -import { time } from '../../../src/utils.mjs' -import { constrainGlobalBandwidth, constrainIndividualBandwidth, constrainLifetime, constrainTraffic } from '../../../src/relay/constraints.mjs' - -describe('Relay constraints', () => { - describe('constrainIndividualBandwidth', () => { - it('should pass', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(16)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainIndividualBandwidth(relayHandler, 16) - - // When + Then - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - }) - - it('should throw on too much data', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(32)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainIndividualBandwidth(relayHandler, 16) - - // When + Then - assert.throws(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - }) - }) - - describe('constrainGlobalBandwidth', () => { - it('should pass', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(4)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainGlobalBandwidth(relayHandler, 16) - - // When + Then - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[1], relayTable[0], message) - ) - }) - - it('should throw', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(12)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainGlobalBandwidth(relayHandler, 16) - - // When + Then - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - assert.throws(() => - relayHandler.emit('transmit', relayTable[1], relayTable[0], message) - ) - }) - }) - - describe('constrainLifetime', () => { - it('should pass', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001, - created: time() - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(4)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainLifetime(relayHandler, 4) - - // When + Then - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - }) - - it('should throw', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001, - created: time() - 16 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(4)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainLifetime(relayHandler, 4) - - // When + Then - assert.throws(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - }) - }) - - describe('constrainTraffic', () => { - it('should pass', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(4)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainTraffic(relayHandler, 16) - - // When + Then - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[1], relayTable[0], message) - ) - }) - - it('should throw', () => { - // Given - const relayTable = [ - new RelayEntry({ - address: new NetAddress({ address: '37.89.0.5', port: 32467 }), - port: 10001 - }), - - new RelayEntry({ - address: new NetAddress({ address: '57.13.0.9', port: 45357 }), - port: 10002 - }), - ] - - const message = Buffer.from('a'.repeat(12)) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - constrainTraffic(relayHandler, 16) - - // When + Then - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[0], relayTable[1], message) - ) - assert.doesNotThrow(() => - relayHandler.emit('transmit', relayTable[1], relayTable[0], message) - ) - assert.throws(() => - relayHandler.emit('transmit', relayTable[1], relayTable[0], message) - ) - }) - }) -}) diff --git a/test/spec/relay/constraints.test.ts b/test/spec/relay/constraints.test.ts new file mode 100644 index 0000000..cbdac77 --- /dev/null +++ b/test/spec/relay/constraints.test.ts @@ -0,0 +1,264 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import sinon from "sinon"; +import { RelayEntry } from "../../../src/relay/relay.entry.ts"; +import { NetAddress } from "../../../src/relay/net.address.ts"; +import { UDPRelayHandler } from "../../../src/relay/udp.relay.handler.ts"; +import { time } from "../../../src/utils.ts"; +import { + constrainGlobalBandwidth, + constrainIndividualBandwidth, + constrainLifetime, + constrainTraffic, +} from "../../../src/relay/constraints.ts"; + +describe("Relay constraints", () => { + describe("constrainIndividualBandwidth", () => { + it("should pass", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(16)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainIndividualBandwidth(relayHandler, 16); + + // When + Then + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + }); + + it("should throw on too much data", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(32)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainIndividualBandwidth(relayHandler, 16); + + // When + Then + assert.throws(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + }); + }); + + describe("constrainGlobalBandwidth", () => { + it("should pass", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(4)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainGlobalBandwidth(relayHandler, 16); + + // When + Then + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[1], relayTable[0], message), + ); + }); + + it("should throw", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(12)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainGlobalBandwidth(relayHandler, 16); + + // When + Then + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + assert.throws(() => + relayHandler.emit("transmit", relayTable[1], relayTable[0], message), + ); + }); + }); + + describe("constrainLifetime", () => { + it("should pass", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + created: time(), + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(4)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainLifetime(relayHandler, 4); + + // When + Then + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + }); + + it("should throw", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + created: time() - 16, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(4)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainLifetime(relayHandler, 4); + + // When + Then + assert.throws(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + }); + }); + + describe("constrainTraffic", () => { + it("should pass", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(4)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainTraffic(relayHandler, 16); + + // When + Then + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[1], relayTable[0], message), + ); + }); + + it("should throw", () => { + // Given + const relayTable = [ + new RelayEntry({ + address: new NetAddress({ address: "37.89.0.5", port: 32467 }), + port: 10001, + }), + + new RelayEntry({ + address: new NetAddress({ address: "57.13.0.9", port: 45357 }), + port: 10002, + }), + ]; + + const message = Buffer.from("a".repeat(12)); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + constrainTraffic(relayHandler, 16); + + // When + Then + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[0], relayTable[1], message), + ); + assert.doesNotThrow(() => + relayHandler.emit("transmit", relayTable[1], relayTable[0], message), + ); + assert.throws(() => + relayHandler.emit("transmit", relayTable[1], relayTable[0], message), + ); + }); + }); +}); diff --git a/test/spec/relay/dynamic.relaying.test.mjs b/test/spec/relay/dynamic.relaying.test.mjs deleted file mode 100644 index 9b049af..0000000 --- a/test/spec/relay/dynamic.relaying.test.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import { beforeEach, afterEach, describe, it } from 'node:test' -import assert from 'node:assert' -import sinon from 'sinon' -import { UDPRelayHandler } from '../../../src/relay/udp.relay.handler.mjs' -import { sleep } from '../../../src/utils.mjs' -import { useDynamicRelay } from '../../../src/relay/dynamic.relaying.mjs' -import { RelayEntry } from '../../../src/relay/relay.entry.mjs' -import { NetAddress } from '../../../src/relay/net.address.mjs' -import { UDPSocketPool } from '../../../src/relay/udp.socket.pool.mjs' - -describe('DynamicRelaying', () => { - let clock - - beforeEach(() => { - clock = sinon.useFakeTimers() - }) - - it('should create relay', async () => { - // Given - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.returns(10000) - - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - sinon.stub(relayHandler, 'socketPool').value(socketPool) - - relayHandler.createRelay.resolves(true) - useDynamicRelay(relayHandler) - - const senderRelay = undefined - const targetRelay = new RelayEntry({ - address: new NetAddress({ address: '87.54.0.16', port: 16752 }), - port: 10007 - }) - const senderAddress = new NetAddress({ address: '97.32.4.16', port: 32775 }) - const targetPort = targetRelay.port - const messages = [ - 'hello', 'world', 'use', 'noray' - ].map(message => Buffer.from(message)) - - // When - messages.forEach(message => - relayHandler.emit('drop', senderRelay, targetRelay, senderAddress, targetPort, message) - ) - clock.restore() - await sleep(0.05) // Wait for relay to be created - clock = sinon.useFakeTimers() - - // Then - const createdRelay = relayHandler.createRelay.lastCall.args[0] - assert(createdRelay, 'Relay was not created!') - assert.equal(createdRelay.address, senderAddress) - assert.equal(createdRelay.port, 10000) - - const sent = relayHandler.relay.getCalls().map(call => call.args[0]?.toString()) - messages.forEach(message => - assert( - sent.includes(message.toString()), - `Message "${message.toString()}" was not sent!` - ) - ) - }) - - it('should ignore known sender', async () => { - // Given - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - useDynamicRelay(relayHandler) - - const senderRelay = new RelayEntry({ - address: new NetAddress({ address: '87.54.0.16', port: 16752 }), - port: 10007 - }) - - const targetRelay = undefined - const senderAddress = new NetAddress(senderRelay.address) - const targetPort = 10057 - const messages = [ - 'hello', 'world', 'use', 'noray' - ].map(message => Buffer.from(message)) - - // When - messages.forEach(message => - relayHandler.emit('drop', senderRelay, targetRelay, senderAddress, targetPort, message) - ) - clock.restore() - await sleep(0.05) // Wait for relay to be created - clock = sinon.useFakeTimers() - - // Then - assert(relayHandler.createRelay.notCalled) - assert(relayHandler.relay.notCalled) - }) - - it('should ignore unknown target', async () => { - // Given - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - relayHandler.on.callThrough() - relayHandler.emit.callThrough() - - useDynamicRelay(relayHandler) - - const senderRelay = undefined - const targetRelay = undefined - const senderAddress = new NetAddress({ address: '87.54.0.16', port: 16752 }) - const targetPort = 10057 - const messages = [ - 'hello', 'world', 'use', 'noray' - ].map(message => Buffer.from(message)) - - // When - messages.forEach(message => - relayHandler.emit('drop', senderRelay, targetRelay, senderAddress, targetPort, message) - ) - clock.restore() - await sleep(0.05) // Wait for relay to be created - clock = sinon.useFakeTimers() - - // Then - assert(relayHandler.createRelay.notCalled) - assert(relayHandler.relay.notCalled) - }) - - afterEach(() => { - clock.restore() - }) -}) diff --git a/test/spec/relay/dynamic.relaying.test.ts b/test/spec/relay/dynamic.relaying.test.ts new file mode 100644 index 0000000..20cd572 --- /dev/null +++ b/test/spec/relay/dynamic.relaying.test.ts @@ -0,0 +1,159 @@ +import { beforeEach, afterEach, describe, it } from "node:test"; +import assert from "node:assert"; +import sinon from "sinon"; +import { UDPRelayHandler } from "../../../src/relay/udp.relay.handler.ts"; +import { sleep } from "../../../src/utils.ts"; +import { useDynamicRelay } from "../../../src/relay/dynamic.relaying.ts"; +import { RelayEntry } from "../../../src/relay/relay.entry.ts"; +import { NetAddress } from "../../../src/relay/net.address.ts"; +import { UDPSocketPool } from "../../../src/relay/udp.socket.pool.ts"; + +describe("DynamicRelaying", () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + it("should create relay", async () => { + // Given + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.returns(10000); + + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + sinon.stub(relayHandler, "socketPool").value(socketPool); + + relayHandler.createRelay.resolves(true); + useDynamicRelay(relayHandler); + + const senderRelay = undefined; + const targetRelay = new RelayEntry({ + address: new NetAddress({ address: "87.54.0.16", port: 16752 }), + port: 10007, + }); + const senderAddress = new NetAddress({ + address: "97.32.4.16", + port: 32775, + }); + const targetPort = targetRelay.port; + const messages = ["hello", "world", "use", "noray"].map((message) => + Buffer.from(message), + ); + + // When + messages.forEach((message) => + relayHandler.emit( + "drop", + senderRelay, + targetRelay, + senderAddress, + targetPort, + message, + ), + ); + clock.restore(); + await sleep(0.05); // Wait for relay to be created + clock = sinon.useFakeTimers(); + + // Then + const createdRelay = relayHandler.createRelay.lastCall.args[0]; + assert(createdRelay, "Relay was not created!"); + assert.equal(createdRelay.address, senderAddress); + assert.equal(createdRelay.port, 10000); + + const sent = relayHandler.relay + .getCalls() + .map((call) => call.args[0]?.toString()); + messages.forEach((message) => + assert( + sent.includes(message.toString()), + `Message "${message.toString()}" was not sent!`, + ), + ); + }); + + it("should ignore known sender", async () => { + // Given + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + useDynamicRelay(relayHandler); + + const senderRelay = new RelayEntry({ + address: new NetAddress({ address: "87.54.0.16", port: 16752 }), + port: 10007, + }); + + const targetRelay = undefined; + const senderAddress = new NetAddress(senderRelay.address); + const targetPort = 10057; + const messages = ["hello", "world", "use", "noray"].map((message) => + Buffer.from(message), + ); + + // When + messages.forEach((message) => + relayHandler.emit( + "drop", + senderRelay, + targetRelay, + senderAddress, + targetPort, + message, + ), + ); + clock.restore(); + await sleep(0.05); // Wait for relay to be created + clock = sinon.useFakeTimers(); + + // Then + assert(relayHandler.createRelay.notCalled); + assert(relayHandler.relay.notCalled); + }); + + it("should ignore unknown target", async () => { + // Given + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + relayHandler.on.callThrough(); + relayHandler.emit.callThrough(); + + useDynamicRelay(relayHandler); + + const senderRelay = undefined; + const targetRelay = undefined; + const senderAddress = new NetAddress({ + address: "87.54.0.16", + port: 16752, + }); + const targetPort = 10057; + const messages = ["hello", "world", "use", "noray"].map((message) => + Buffer.from(message), + ); + + // When + messages.forEach((message) => + relayHandler.emit( + "drop", + senderRelay, + targetRelay, + senderAddress, + targetPort, + message, + ), + ); + clock.restore(); + await sleep(0.05); // Wait for relay to be created + clock = sinon.useFakeTimers(); + + // Then + assert(relayHandler.createRelay.notCalled); + assert(relayHandler.relay.notCalled); + }); + + afterEach(() => { + clock.restore(); + }); +}); diff --git a/test/spec/relay/relay.entry.test.mjs b/test/spec/relay/relay.entry.test.mjs deleted file mode 100644 index 2a6c12b..0000000 --- a/test/spec/relay/relay.entry.test.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import { RelayEntry } from '../../../src/relay/relay.entry.mjs' -import { NetAddress } from '../../../src/relay/net.address.mjs' - -describe('RelayEntry', () => { - describe('equals', () => { - const cases = [ - [ - 'same should equal', - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1000 }), port: 2000 }), - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1000 }), port: 2000 }), - true - ], - [ - 'different port should equal', - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1000 }), port: 2000 }), - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1000 }), port: 2010 }), - true - ], - [ - 'different address host should not equal', - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1000 }), port: 2000 }), - new RelayEntry({ address: new NetAddress({ address: "host2", port: 1000 }), port: 2000 }), - false - ], - [ - 'different address port should not equal', - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1000 }), port: 2000 }), - new RelayEntry({ address: new NetAddress({ address: "host1", port: 1020 }), port: 2000 }), - false - ], - ] - - cases.forEach(([name, a, b, expected]) => { - it(name, () => { assert.equal(a.equals(b), expected) }) - }) - }) -}) diff --git a/test/spec/relay/relay.entry.test.ts b/test/spec/relay/relay.entry.test.ts new file mode 100644 index 0000000..c87ca05 --- /dev/null +++ b/test/spec/relay/relay.entry.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { RelayEntry } from "../../../src/relay/relay.entry.ts"; +import { NetAddress } from "../../../src/relay/net.address.ts"; + +describe("RelayEntry", () => { + describe("equals", () => { + const cases = [ + [ + "same should equal", + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1000 }), + port: 2000, + }), + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1000 }), + port: 2000, + }), + true, + ], + [ + "different port should equal", + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1000 }), + port: 2000, + }), + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1000 }), + port: 2010, + }), + true, + ], + [ + "different address host should not equal", + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1000 }), + port: 2000, + }), + new RelayEntry({ + address: new NetAddress({ address: "host2", port: 1000 }), + port: 2000, + }), + false, + ], + [ + "different address port should not equal", + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1000 }), + port: 2000, + }), + new RelayEntry({ + address: new NetAddress({ address: "host1", port: 1020 }), + port: 2000, + }), + false, + ], + ] as Array<[string, RelayEntry, RelayEntry, boolean]>; + + cases.forEach(([name, a, b, expected]) => { + it(name, () => { + assert.equal(a.equals(b), expected); + }); + }); + }); +}); diff --git a/test/spec/relay/udp.relay.cleanup.test.mjs b/test/spec/relay/udp.relay.cleanup.test.mjs deleted file mode 100644 index 61acc29..0000000 --- a/test/spec/relay/udp.relay.cleanup.test.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import sinon from 'sinon' -import { time } from '../../../src/utils.mjs' -import { RelayEntry } from '../../../src/relay/relay.entry.mjs' -import { NetAddress } from '../../../src/relay/net.address.mjs' -import { UDPRelayHandler } from '../../../src/relay/udp.relay.handler.mjs' -import { cleanupUdpRelayTable } from '../../../src/relay/udp.relay.cleanup.mjs' - -describe('cleanupUdpRelayTable', () => { - it('should free old relays', () => { - // Given - const origin = time() - const timeout = 2 - const expired = origin - timeout - 1 - - console.log({ - origin, timeout, expired - }) - - const freshRelay = new RelayEntry({ - address: new NetAddress({ address: '88.57.0.2', port: 32276 }), - port: 10001, - lastReceived: origin, - lastSent: origin - }) - - const oldSendRelay = new RelayEntry({ - address: new NetAddress({ address: '88.57.0.3', port: 32276 }), - port: 10002, - lastReceived: origin, - lastSent: expired - }) - - const oldReceiveRelay = new RelayEntry({ - address: new NetAddress({ address: '88.57.0.3', port: 32276 }), - port: 10003, - lastReceived: expired, - lastSent: origin - }) - - const expiredRelay = new RelayEntry({ - address: new NetAddress({ address: '88.57.0.4', port: 32276 }), - port: 10004, - lastReceived: expired, - lastSent: expired - }) - - const relayTable = [freshRelay, oldSendRelay, oldReceiveRelay, expiredRelay] - const relayHandler = sinon.createStubInstance(UDPRelayHandler) - sinon.stub(relayHandler, 'relayTable').value(relayTable) - - // When - cleanupUdpRelayTable(relayHandler, timeout) - - // Then - assert(relayHandler.freeRelay.neverCalledWith(freshRelay), 'Fresh relay was freed!') - assert(relayHandler.freeRelay.neverCalledWith(oldSendRelay), 'Old send relay was freed!') - assert(relayHandler.freeRelay.neverCalledWith(oldReceiveRelay), 'Old receive relay was freed!') - assert(relayHandler.freeRelay.calledWith(expiredRelay), 'Expired relay was not freed!') - }) -}) diff --git a/test/spec/relay/udp.relay.cleanup.test.ts b/test/spec/relay/udp.relay.cleanup.test.ts new file mode 100644 index 0000000..5ed770e --- /dev/null +++ b/test/spec/relay/udp.relay.cleanup.test.ts @@ -0,0 +1,81 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import sinon from "sinon"; +import { time } from "../../../src/utils.ts"; +import { RelayEntry } from "../../../src/relay/relay.entry.ts"; +import { NetAddress } from "../../../src/relay/net.address.ts"; +import { UDPRelayHandler } from "../../../src/relay/udp.relay.handler.ts"; +import { cleanupUdpRelayTable } from "../../../src/relay/udp.relay.cleanup.ts"; + +describe("cleanupUdpRelayTable", () => { + it("should free old relays", () => { + // Given + const origin = time(); + const timeout = 2; + const expired = origin - timeout - 1; + + console.log({ + origin, + timeout, + expired, + }); + + const freshRelay = new RelayEntry({ + address: new NetAddress({ address: "88.57.0.2", port: 32276 }), + port: 10001, + lastReceived: origin, + lastSent: origin, + }); + + const oldSendRelay = new RelayEntry({ + address: new NetAddress({ address: "88.57.0.3", port: 32276 }), + port: 10002, + lastReceived: origin, + lastSent: expired, + }); + + const oldReceiveRelay = new RelayEntry({ + address: new NetAddress({ address: "88.57.0.3", port: 32276 }), + port: 10003, + lastReceived: expired, + lastSent: origin, + }); + + const expiredRelay = new RelayEntry({ + address: new NetAddress({ address: "88.57.0.4", port: 32276 }), + port: 10004, + lastReceived: expired, + lastSent: expired, + }); + + const relayTable = [ + freshRelay, + oldSendRelay, + oldReceiveRelay, + expiredRelay, + ]; + const relayHandler = sinon.createStubInstance(UDPRelayHandler); + sinon.stub(relayHandler, "relayTable").value(relayTable); + + // When + cleanupUdpRelayTable(relayHandler, timeout); + + // Then + assert( + relayHandler.freeRelay.neverCalledWith(freshRelay), + "Fresh relay was freed!", + ); + assert( + relayHandler.freeRelay.neverCalledWith(oldSendRelay), + "Old send relay was freed!", + ); + assert( + relayHandler.freeRelay.neverCalledWith(oldReceiveRelay), + "Old receive relay was freed!", + ); + assert( + relayHandler.freeRelay.calledWith(expiredRelay), + "Expired relay was not freed!", + ); + }); +}); diff --git a/test/spec/relay/udp.relay.handler.test.mjs b/test/spec/relay/udp.relay.handler.test.mjs deleted file mode 100644 index 7a01ee8..0000000 --- a/test/spec/relay/udp.relay.handler.test.mjs +++ /dev/null @@ -1,328 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import sinon from 'sinon' -import dgram from 'node:dgram' -import { UDPSocketPool } from '../../../src/relay/udp.socket.pool.mjs' -import { RelayEntry } from '../../../src/relay/relay.entry.mjs' -import { NetAddress } from '../../../src/relay/net.address.mjs' -import { UDPRelayHandler } from '../../../src/relay/udp.relay.handler.mjs' - -describe('UDPRelayHandler', () => { - describe('createRelay', () => { - it('should create relay', async () => { - // Given - const handler = sinon.stub() - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.returns(10001) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const relay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: '32279' - }) - }) - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - - relayHandler.on('create', handler) - - // When - const result = await relayHandler.createRelay(relay) - - // Then - assert.deepEqual(relayHandler.relayTable, [relay]) - assert(relay.port, 'No port assigned to relay!') - assert(handler.calledWith(relay), 'Create event not emitted!') - }) - - it('should ignore if relay exists', async () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const relay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: '32279' - }) - }) - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - await relayHandler.createRelay(relay) - - // When - const result = await relayHandler.createRelay(relay) - - // When - assert.equal(result, relay) - assert.deepEqual(relayHandler.relayTable, [relay]) - }) - }) - - describe('freeRelay', () => { - it('should free relay', async () => { - // Given - const handler = sinon.stub() - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.returns(10001) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const relay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: '32279' - }) - }) - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - await relayHandler.createRelay(relay) - relayHandler.on('destroy', handler) - - // When - const result = relayHandler.freeRelay(relay) - - // When - assert.equal(result, true) - assert(socketPool.returnPort.calledOnceWith(10001)) - assert.deepEqual(relayHandler.relayTable, []) - assert(handler.calledWith(relay), 'Destroy event not emitted!') - }) - - it('should ignore unknown', async () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const relay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: 32279 - }) - }) - - const unknownRelay = new RelayEntry({ - address: new NetAddress({ - address: '89.45.0.109', - port: 32279 - }) - }) - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - await relayHandler.createRelay(relay) - - // When - const result = relayHandler.freeRelay(unknownRelay) - - // When - assert.equal(result, false) - assert(socketPool.deallocatePort.notCalled) - assert.deepEqual(relayHandler.relayTable, [relay]) - }) - }) - - describe('relay', () => { - it('should relay', async () => { - // Given - const message = Buffer.from('Hello!', 'utf-8') - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.onFirstCall().returns(10001) - socketPool.getPort.onSecondCall().returns(10002) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - const handler = sinon.stub() - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - relayHandler.on('transmit', handler) - - await relayHandler.createRelay(new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.17', - port: 32279 - }) - })) - - await relayHandler.createRelay(new RelayEntry({ - address: new NetAddress({ - address: '88.59.62.107', - port: 65227 - }) - })) - socketPool.getSocket.resetHistory() - - // When - const success = relayHandler.relay(message, new NetAddress({ - address: '88.59.62.107', - port: 65227 - }), 10001) - - // Then - assert(success, 'Relay failed!') - assert(socketPool.getSocket.calledOnceWith(10002), 'Socket not queried!') - assert(socket.send.calledWith(message), 'Message not sent!') - assert(handler.calledOnce, 'Transmit event not emitted!') - }) - - it('should ignore unknown address', async () => { - // Given - const message = Buffer.from('Hello!', 'utf-8') - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.onFirstCall().returns(10001) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const dropHandler = sinon.spy() - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - - await relayHandler.createRelay(new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.17', - port: 32279 - }) - })) - relayHandler.on('drop', dropHandler) - socketPool.getSocket.resetHistory() - - // When - const success = relayHandler.relay(message, new NetAddress({ - address: '88.59.62.107', - port: 65227 - }), 10001) - - // Then - assert(!success, 'Relay succeeded?') - assert(socketPool.getSocket.notCalled, 'Socket queried!') - assert(socket.send.notCalled, 'Message sent?') - assert(dropHandler.calledOnce, 'Drop event not emitted!') - }) - it('should ignore on missing socket', async () => { - // Given - const message = Buffer.from('Hello!', 'utf-8') - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.onFirstCall().returns(10001) - socketPool.getPort.onSecondCall().returns(10002) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - - await relayHandler.createRelay(new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.17', - port: 32279 - }) - })) - - await relayHandler.createRelay(new RelayEntry({ - address: new NetAddress({ - address: '88.59.62.107', - port: 65227 - }) - })) - socketPool.getSocket.resetHistory() - socketPool.getSocket.returns(undefined) - - // When - const success = relayHandler.relay(message, new NetAddress({ - address: '88.59.62.107', - port: 65227 - }), 10001) - - // Then - assert(!success, 'Relay succeeded?') - assert(socketPool.getSocket.called, 'Socket not queried!') - assert(socket.send.notCalled, 'Message sent?') - }) - }) - describe('hasRelay', () => { - it('should not have relay', async () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.returns(10001) - socketPool.getPort.returns(10002) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const createdRelay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: 32279 - }) - }) - - const testRelay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: 49152 - }) - }) - - const relayHandler = new UDPRelayHandler({ - socketPool - }) - - await relayHandler.createRelay(createdRelay) - - // When + Then - assert(!relayHandler.hasRelay(testRelay)) - }) - it('should have relay', async () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - const socketPool = sinon.createStubInstance(UDPSocketPool) - socketPool.getPort.onFirstCall().returns(10001) - socketPool.getPort.onSecondCall().returns(10002) - socketPool.getSocket.returns(socket) - socket.removeAllListeners.returnsThis() - - const createdRelay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: 32279 - }) - }) - - const testRelay = new RelayEntry({ - address: new NetAddress({ - address: '88.57.0.107', - port: 32279 - }) - }) - const relayHandler = new UDPRelayHandler({ - socketPool - }) - - await relayHandler.createRelay(createdRelay) - - // When + Then - assert(relayHandler.hasRelay(testRelay)) - }) - }) -}) diff --git a/test/spec/relay/udp.relay.handler.test.ts b/test/spec/relay/udp.relay.handler.test.ts new file mode 100644 index 0000000..ffbd7fb --- /dev/null +++ b/test/spec/relay/udp.relay.handler.test.ts @@ -0,0 +1,364 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import sinon from "sinon"; +import dgram from "node:dgram"; +import { UDPSocketPool } from "../../../src/relay/udp.socket.pool.ts"; +import { RelayEntry } from "../../../src/relay/relay.entry.ts"; +import { NetAddress } from "../../../src/relay/net.address.ts"; +import { UDPRelayHandler } from "../../../src/relay/udp.relay.handler.ts"; + +describe("UDPRelayHandler", () => { + describe("createRelay", () => { + it("should create relay", async () => { + // Given + const handler = sinon.stub(); + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.returns(10001); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const relay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + + relayHandler.on("create", handler); + + // When + relayHandler.createRelay(relay); + + // Then + assert.deepEqual(relayHandler.relayTable, [relay]); + assert(relay.port, "No port assigned to relay!"); + assert(handler.calledWith(relay), "Create event not emitted!"); + }); + + it("should ignore if relay exists", async () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const relay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + relayHandler.createRelay(relay); + + // When + const result = relayHandler.createRelay(relay); + + // When + assert.equal(result, relay); + assert.deepEqual(relayHandler.relayTable, [relay]); + }); + }); + + describe("freeRelay", () => { + it("should free relay", async () => { + // Given + const handler = sinon.stub(); + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.returns(10001); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const relay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + relayHandler.createRelay(relay); + relayHandler.on("destroy", handler); + + // When + const result = relayHandler.freeRelay(relay); + + // When + assert.equal(result, true); + assert(socketPool.returnPort.calledOnceWith(10001)); + assert.deepEqual(relayHandler.relayTable, []); + assert(handler.calledWith(relay), "Destroy event not emitted!"); + }); + + it("should ignore unknown", async () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const relay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + + const unknownRelay = new RelayEntry({ + port: 56537, + address: new NetAddress({ + address: "89.45.0.109", + port: 32279, + }), + }); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + relayHandler.createRelay(relay); + + // When + const result = relayHandler.freeRelay(unknownRelay); + + // When + assert.equal(result, false); + assert(socketPool.deallocatePort.notCalled); + assert.deepEqual(relayHandler.relayTable, [relay]); + }); + }); + + describe("relay", () => { + it("should relay", async () => { + // Given + const message = Buffer.from("Hello!", "utf-8"); + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.onFirstCall().returns(10001); + socketPool.getPort.onSecondCall().returns(10002); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + const handler = sinon.stub(); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + relayHandler.on("transmit", handler); + + relayHandler.createRelay( + new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.17", + port: 32279, + }), + }), + ); + + relayHandler.createRelay( + new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.59.62.107", + port: 65227, + }), + }), + ); + socketPool.getSocket.resetHistory(); + + // When + const success = relayHandler.relay( + message, + new NetAddress({ + address: "88.59.62.107", + port: 65227, + }), + 10001, + ); + + // Then + assert(success, "Relay failed!"); + assert(socketPool.getSocket.calledOnceWith(10002), "Socket not queried!"); + assert(socket.send.calledWith(message), "Message not sent!"); + assert(handler.calledOnce, "Transmit event not emitted!"); + }); + + it("should ignore unknown address", async () => { + // Given + const message = Buffer.from("Hello!", "utf-8"); + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.onFirstCall().returns(10001); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const dropHandler = sinon.spy(); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + + relayHandler.createRelay( + new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.17", + port: 32279, + }), + }), + ); + relayHandler.on("drop", dropHandler); + socketPool.getSocket.resetHistory(); + + // When + const success = relayHandler.relay( + message, + new NetAddress({ + address: "88.59.62.107", + port: 65227, + }), + 10001, + ); + + // Then + assert(!success, "Relay succeeded?"); + assert(socketPool.getSocket.notCalled, "Socket queried!"); + assert(socket.send.notCalled, "Message sent?"); + assert(dropHandler.calledOnce, "Drop event not emitted!"); + }); + it("should ignore on missing socket", async () => { + // Given + const message = Buffer.from("Hello!", "utf-8"); + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.onFirstCall().returns(10001); + socketPool.getPort.onSecondCall().returns(10002); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + + relayHandler.createRelay( + new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.17", + port: 32279, + }), + }), + ); + + relayHandler.createRelay( + new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.59.62.107", + port: 65227, + }), + }), + ); + socketPool.getSocket.resetHistory(); + socketPool.getSocket.returns(undefined); + + // When + const success = relayHandler.relay( + message, + new NetAddress({ + address: "88.59.62.107", + port: 65227, + }), + 10001, + ); + + // Then + assert(!success, "Relay succeeded?"); + assert(socketPool.getSocket.called, "Socket not queried!"); + assert(socket.send.notCalled, "Message sent?"); + }); + }); + describe("hasRelay", () => { + it("should not have relay", async () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.returns(10001); + socketPool.getPort.returns(10002); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const createdRelay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + + const testRelay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 49152, + }), + }); + + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + + relayHandler.createRelay(createdRelay); + + // When + Then + assert(!relayHandler.hasRelay(testRelay)); + }); + it("should have relay", async () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + const socketPool = sinon.createStubInstance(UDPSocketPool); + socketPool.getPort.onFirstCall().returns(10001); + socketPool.getPort.onSecondCall().returns(10002); + socketPool.getSocket.returns(socket); + socket.removeAllListeners.returnsThis(); + + const createdRelay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + + const testRelay = new RelayEntry({ + port: 57789, + address: new NetAddress({ + address: "88.57.0.107", + port: 32279, + }), + }); + const relayHandler = new UDPRelayHandler({ + socketPool, + }); + + relayHandler.createRelay(createdRelay); + + // When + Then + assert(relayHandler.hasRelay(testRelay)); + }); + }); +}); diff --git a/test/spec/relay/udp.remote.registrar.test.mjs b/test/spec/relay/udp.remote.registrar.test.mjs deleted file mode 100644 index 10963ad..0000000 --- a/test/spec/relay/udp.remote.registrar.test.mjs +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, beforeEach, afterEach } from 'node:test' -import assert from 'node:assert' -import sinon from 'sinon' -import dgram from 'node:dgram' -import { UDPRemoteRegistrar } from '../../../src/relay/udp.remote.registrar.mjs' -import { HostRepository } from '../../../src/hosts/host.repository.mjs' -import { HostEntity } from '../../../src/hosts/host.entity.mjs' - -describe('UDPRemoteRegistrar', () => { - /** @type {sinon.SinonFakeTimers} */ - let clock - - /** @type {sinon.SinonStubbedInstance} */ - let hostRepository - /** @type {sinon.SinonStubbedInstance} */ - let socket - - /** @type {UDPRemoteRegistrar} */ - let remoteRegistrar - - /** @type {HostEntity} */ - let host - - beforeEach(() => { - host = new HostEntity({ - oid: 'h0001', - pid: 'p0001' - }) - clock = sinon.useFakeTimers() - - hostRepository = sinon.createStubInstance(HostRepository) - socket = sinon.createStubInstance(dgram.Socket) - - hostRepository.findByPid.withArgs(host.pid).returns(host) - socket.bind.callsArg(2) // Instantly resolve on bind - socket.address.returns({ - address: '127.0.0.1', - port: 32768 - }) - - remoteRegistrar = new UDPRemoteRegistrar({ - hostRepository, socket - }) - }) - - it('should succeed', async () => { - // Given - const msg = Buffer.from(host.pid) - const rinfo = { address: '88.57.0.3', port: 32745 } - - await remoteRegistrar.listen() - const messageHandler = socket.on.lastCall.callback - - // When - await messageHandler(msg, rinfo) - - // Then - assert.deepEqual( - socket.send.lastCall?.args, - ['OK', rinfo.port, rinfo.address] - ) - assert.equal(host.rinfo, rinfo) - }) - - it('should fail on unknown pid', async () => { - // Given - const msg = Buffer.from(host.pid) - const rinfo = { address: '88.57.0.3', port: 32745 } - - await remoteRegistrar.listen() - const messageHandler = socket.on.lastCall.callback - - hostRepository.findByPid.withArgs(host.pid).returns(undefined) - - // When - await messageHandler(msg, rinfo) - - // Then - assert.deepEqual( - socket.send.lastCall?.args, - ['Unknown host pid!', rinfo.port, rinfo.address] - ) - }) - - it('should fail on throw', async () => { - // Given - const msg = Buffer.from(host.pid) - const rinfo = { address: '88.57.0.3', port: 32745 } - - await remoteRegistrar.listen() - const messageHandler = socket.on.lastCall.callback - - socket.send.onFirstCall().throws(new Error('Test')) - - // When - await messageHandler(msg, rinfo) - - // Then - assert.deepEqual( - socket.send.lastCall?.args, - ['Test', rinfo.port, rinfo.address] - ) - }) - - afterEach(() => { - clock.restore() - }) -}) diff --git a/test/spec/relay/udp.remote.registrar.test.ts b/test/spec/relay/udp.remote.registrar.test.ts new file mode 100644 index 0000000..31588bf --- /dev/null +++ b/test/spec/relay/udp.remote.registrar.test.ts @@ -0,0 +1,109 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert"; +import * as net from "node:net"; +import sinon from "sinon"; +import dgram from "node:dgram"; +import { UDPRemoteRegistrar } from "../../../src/relay/udp.remote.registrar.ts"; +import { HostRepository } from "../../../src/hosts/host.repository.ts"; +import { type HostEntity } from "../../../src/hosts/host.entity.ts"; + +describe("UDPRemoteRegistrar", () => { + let clock: sinon.SinonFakeTimers; + let hostRepository: sinon.SinonStubbedInstance; + let socket: sinon.SinonStubbedInstance; + let remoteRegistrar: UDPRemoteRegistrar; + let host: HostEntity; + + beforeEach(() => { + host = { + oid: "h0001", + pid: "p0001", + socket: new net.Socket(), + relay: undefined, + rinfo: undefined, + }; + clock = sinon.useFakeTimers(); + + hostRepository = sinon.createStubInstance(HostRepository); + socket = sinon.createStubInstance(dgram.Socket); + + hostRepository.findByPid.withArgs(host.pid).returns(host); + socket.bind.callsArg(2); // Instantly resolve on bind + socket.address.returns({ + address: "127.0.0.1", + port: 32768, + family: "ipv4", + }); + + remoteRegistrar = new UDPRemoteRegistrar({ + hostRepository, + socket, + }); + }); + + it("should succeed", async () => { + // Given + const msg = Buffer.from(host.pid); + const rinfo = { address: "88.57.0.3", port: 32745 }; + + await remoteRegistrar.listen(); + const messageHandler = socket.on.lastCall.callback!!; + + // When + await messageHandler(msg, rinfo); + + // Then + assert.deepEqual(socket.send.lastCall?.args, [ + "OK", + rinfo.port, + rinfo.address, + ]); + assert.equal(host.rinfo, rinfo); + }); + + it("should fail on unknown pid", async () => { + // Given + const msg = Buffer.from(host.pid); + const rinfo = { address: "88.57.0.3", port: 32745 }; + + await remoteRegistrar.listen(); + const messageHandler = socket.on.lastCall.callback!!; + + hostRepository.findByPid.withArgs(host.pid).returns(undefined); + + // When + await messageHandler(msg, rinfo); + + // Then + assert.deepEqual(socket.send.lastCall?.args, [ + "Unknown host pid!", + rinfo.port, + rinfo.address, + ]); + }); + + it("should fail on throw", async () => { + // Given + const msg = Buffer.from(host.pid); + const rinfo = { address: "88.57.0.3", port: 32745 }; + + await remoteRegistrar.listen(); + const messageHandler = socket.on.lastCall.callback!!; + + socket.send.onFirstCall().throws(new Error("Test")); + + // When + await messageHandler(msg, rinfo); + + // Then + assert.deepEqual(socket.send.lastCall?.args, [ + "Test", + rinfo.port, + rinfo.address, + ]); + }); + + afterEach(() => { + clock.restore(); + }); +}); diff --git a/test/spec/relay/udp.socket.pool.test.mjs b/test/spec/relay/udp.socket.pool.test.mjs deleted file mode 100644 index 59b1a9b..0000000 --- a/test/spec/relay/udp.socket.pool.test.mjs +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import dgram from 'node:dgram' -import sinon from 'sinon' -import { UDPSocketPool } from '../../../src/relay/udp.socket.pool.mjs' - -describe('UDPSocketPool', () => { - describe ('allocatePort', () => { - it('should allocate port', async () => { - // Given - const pool = new UDPSocketPool() - - // When + Then - const port = await pool.allocatePort() - - // Finally - pool.deallocatePort(port) - }) - }) - - describe ('addSocket', () => { - it('should save port', () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - socket.address.returns({ - address: '127.0.0.1', - port: 10001, - family: 'ipv4' - }) - const pool = new UDPSocketPool() - - // When - pool.addSocket(socket) - - // Then - assert.deepEqual(pool.ports, [10001]) - assert.equal(pool.getSocket(10001), socket) - }) - }) - - describe('deallocatePort', () => { - it('should call close', () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - socket.address.returns({ - address: '0.0.0.0', - port: 7879, - family: 'IPv4' - }) - - const pool = new UDPSocketPool() - pool.addSocket(socket) - - // When - pool.deallocatePort(7879) - - // Then - assert(socket.close.calledOnce) - assert(!pool.ports.includes(7879)) - }) - - it('should ignore unknown port', () => { - // Given - const socket = sinon.createStubInstance(dgram.Socket) - socket.address.returns({ - address: '0.0.0.0', - port: 7879, - family: 'IPv4' - }) - - const pool = new UDPSocketPool() - pool.addSocket(socket) - - // When - pool.deallocatePort(7876) - - // Then - assert(socket.close.notCalled) - }) - }) - - describe ('getPort', () => { - it('should return allocated', async () => { - // Given - const pool = new UDPSocketPool() - const expected = await pool.allocatePort() - - // When - const actual = pool.getPort() - - // Then - assert(!pool.hasFreePort()) - assert.equal(actual, expected) - - // Finally - pool.deallocatePort(expected) - }) - - it('should throw if none available', () => { - // Given - const pool = new UDPSocketPool() - - // When + Then - assert(!pool.hasFreePort()) - assert.throws(() => pool.getPort()) - }) - }) - - describe('returnPort', () => { - it('should make port available', async () => { - // Given - const pool = new UDPSocketPool() - await pool.allocatePort() - const port = pool.getPort() - - // When - pool.returnPort(port) - - // Then - assert(pool.hasFreePort()) - - // Finally - pool.deallocatePort(port) - }) - - it('should ignore unknown', async () => { - // Given - const pool = new UDPSocketPool() - - // When + then - assert.doesNotThrow(() => pool.returnPort(65575)) - }) - }) -}) diff --git a/test/spec/relay/udp.socket.pool.test.ts b/test/spec/relay/udp.socket.pool.test.ts new file mode 100644 index 0000000..704f0c8 --- /dev/null +++ b/test/spec/relay/udp.socket.pool.test.ts @@ -0,0 +1,134 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import dgram from "node:dgram"; +import sinon from "sinon"; +import { UDPSocketPool } from "../../../src/relay/udp.socket.pool.ts"; + +describe("UDPSocketPool", () => { + describe("allocatePort", () => { + it("should allocate port", async () => { + // Given + const pool = new UDPSocketPool(); + + // When + Then + const port = await pool.allocatePort(); + + // Finally + pool.deallocatePort(port); + }); + }); + + describe("addSocket", () => { + it("should save port", () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + socket.address.returns({ + address: "127.0.0.1", + port: 10001, + family: "ipv4", + }); + const pool = new UDPSocketPool(); + + // When + pool.addSocket(socket); + + // Then + assert.deepEqual(pool.ports, [10001]); + assert.equal(pool.getSocket(10001), socket); + }); + }); + + describe("deallocatePort", () => { + it("should call close", () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + socket.address.returns({ + address: "0.0.0.0", + port: 7879, + family: "IPv4", + }); + + const pool = new UDPSocketPool(); + pool.addSocket(socket); + + // When + pool.deallocatePort(7879); + + // Then + assert(socket.close.calledOnce); + assert(!pool.ports.includes(7879)); + }); + + it("should ignore unknown port", () => { + // Given + const socket = sinon.createStubInstance(dgram.Socket); + socket.address.returns({ + address: "0.0.0.0", + port: 7879, + family: "IPv4", + }); + + const pool = new UDPSocketPool(); + pool.addSocket(socket); + + // When + pool.deallocatePort(7876); + + // Then + assert(socket.close.notCalled); + }); + }); + + describe("getPort", () => { + it("should return allocated", async () => { + // Given + const pool = new UDPSocketPool(); + const expected = await pool.allocatePort(); + + // When + const actual = pool.getPort(); + + // Then + assert(!pool.hasFreePort()); + assert.equal(actual, expected); + + // Finally + pool.deallocatePort(expected); + }); + + it("should throw if none available", () => { + // Given + const pool = new UDPSocketPool(); + + // When + Then + assert(!pool.hasFreePort()); + assert.throws(() => pool.getPort()); + }); + }); + + describe("returnPort", () => { + it("should make port available", async () => { + // Given + const pool = new UDPSocketPool(); + await pool.allocatePort(); + const port = pool.getPort(); + + // When + pool.returnPort(port); + + // Then + assert(pool.hasFreePort()); + + // Finally + pool.deallocatePort(port); + }); + + it("should ignore unknown", async () => { + // Given + const pool = new UDPSocketPool(); + + // When + then + assert.doesNotThrow(() => pool.returnPort(65575)); + }); + }); +}); diff --git a/test/spec/repository.test.mjs b/test/spec/repository.test.mjs deleted file mode 100644 index 0a0dc08..0000000 --- a/test/spec/repository.test.mjs +++ /dev/null @@ -1,231 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' -import { assignMerger, idFieldMapper, IdInUseError, Repository, UnknownItemError } from '../../src/repository.mjs' - -function TestItem (id, value) { - return { id, value } -} - -function makeRepository () { - return new Repository({ - idMapper: idFieldMapper(), - itemMerger: assignMerger() - }) -} - -describe('Repository', () => { - describe('add', () => { - it('should add item', () => { - // Given - const repository = makeRepository() - const expected = TestItem(0, 'foo') - - // When - const actual = repository.add(expected) - - // Then - assert.deepEqual(actual, expected) - assert.equal([...repository.list()].length, 1) - }) - - it('should reject items with same id', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - const duplicate = TestItem(0, 'bar') - - repository.add(item) - - // When + then - assert.throws(() => repository.add(duplicate), IdInUseError) - }) - }) - - describe('update', () => { - it('should update item', () => { - // Given - const repository = makeRepository() - const update = TestItem(0, 'bar') - repository.add(TestItem(0, 'foo')) - - // When - repository.update(update) - - // Then - const actual = repository.find(update.id) - assert.deepEqual(actual, update) - }) - - it('should reject unknown item', () => { - // Given - const repository = makeRepository() - const update = TestItem(0, 'bar') - - // When + then - assert.throws(() => repository.update(update), UnknownItemError) - }) - }) - - describe('find', () => { - it('should return known item', () => { - // Given - const repository = makeRepository() - const expected = TestItem(0, 'foo') - repository.add(expected) - - // When - const actual = repository.find(expected.id) - - // Then - assert.deepEqual(actual, expected) - }) - - it('should return undefined on unknown', () => { - // Given - const repository = makeRepository() - - // When - const actual = repository.find(0) - - // Then - assert.equal(actual, undefined) - }) - }) - - describe('has', () => { - it('should return true on known', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - repository.add(item) - - // When - const result = repository.has(item.id) - - // Then - assert(result) - }) - - it('should return false on unknown', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - - // When - const result = repository.has(item.id) - - // Then - assert(!result) - }) - }) - - describe('list', () => { - it('should return empty', () => { - // Given - const repository = makeRepository() - const expected = [] - - // When - const actual = [...repository.list()] - - // Then - assert.deepEqual(actual, expected) - }) - - it('should return items', () => { - // Given - const repository = makeRepository() - const expected = [TestItem(0, 'foo'), TestItem(1, 'bar')] - expected.forEach(item => repository.add(item)) - - // When - const actual = [...repository.list()] - - // Then - assert.deepEqual(actual, expected) - }) - }) - - describe('remove', () => { - it('should remove known', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - repository.add(item) - - // When - const didRemove = repository.remove(item.id) - - // Then - assert(didRemove) - assert.equal([...repository.list()].length, 0) - }) - - it('should ignore unknown', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - - // When - const didRemove = repository.remove(item.id) - - // Then - assert(!didRemove) - }) - }) - - describe('hasItem', () => { - it('should return true on known', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - repository.add(item) - - // When - const result = repository.hasItem(item) - - // Then - assert(result) - }) - - it('should return false on unknown', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - - // When - const result = repository.hasItem(item) - - // Then - assert(!result) - }) - }) - - describe('removeItem', () => { - it('should remove known', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - repository.add(item) - - // When - const didRemove = repository.removeItem(item) - - // Then - assert(didRemove) - assert.equal([...repository.list()].length, 0) - }) - - it('should ignore unknown', () => { - // Given - const repository = makeRepository() - const item = TestItem(0, 'foo') - - // When - const didRemove = repository.removeItem(item) - - // Then - assert(!didRemove) - }) - }) -}) diff --git a/test/spec/repository.test.ts b/test/spec/repository.test.ts new file mode 100644 index 0000000..1c66745 --- /dev/null +++ b/test/spec/repository.test.ts @@ -0,0 +1,236 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { + IdInUseError, + Repository, + UnknownItemError, +} from "../../src/repository.ts"; + +interface TestItem { + id: number; + value: string; +} + +function makeRepository() { + return new Repository((it) => it.id!!); +} + +describe("Repository", () => { + describe("add", () => { + it("should add item", () => { + // Given + const repository = makeRepository(); + const expected = { id: 0, value: "foo" } as TestItem; + + // When + const actual = repository.add(expected); + + // Then + assert.deepEqual(actual, expected); + assert.equal([...repository.list()].length, 1); + }); + + it("should reject items with same id", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + const duplicate = { id: 0, value: "bar" } as TestItem; + + repository.add(item); + + // When + then + assert.throws(() => repository.add(duplicate), IdInUseError); + }); + }); + + describe("update", () => { + it("should update item", () => { + // Given + const repository = makeRepository(); + const update = { id: 0, value: "bar" } as TestItem; + repository.add({ id: 0, value: "foo" }); + + // When + repository.update(update); + + // Then + const actual = repository.find(update.id); + assert.deepEqual(actual, update); + }); + + it("should reject unknown item", () => { + // Given + const repository = makeRepository(); + const update = { id: 0, value: "bar" } as TestItem; + + // When + then + assert.throws(() => repository.update(update), UnknownItemError); + }); + }); + + describe("find", () => { + it("should return known item", () => { + // Given + const repository = makeRepository(); + const expected = { id: 0, value: "foo" } as TestItem; + repository.add(expected); + + // When + const actual = repository.find(expected.id); + + // Then + assert.deepEqual(actual, expected); + }); + + it("should return undefined on unknown", () => { + // Given + const repository = makeRepository(); + + // When + const actual = repository.find(0); + + // Then + assert.equal(actual, undefined); + }); + }); + + describe("has", () => { + it("should return true on known", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + repository.add(item); + + // When + const result = repository.has(item.id); + + // Then + assert(result); + }); + + it("should return false on unknown", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + + // When + const result = repository.has(item.id); + + // Then + assert(!result); + }); + }); + + describe("list", () => { + it("should return empty", () => { + // Given + const repository = makeRepository(); + const expected = [] as TestItem[]; + + // When + const actual = [...repository.list()]; + + // Then + assert.deepEqual(actual, expected); + }); + + it("should return items", () => { + // Given + const repository = makeRepository(); + const expected = [ + { id: 0, value: "foo" }, + { id: 1, value: "bar" }, + ] as TestItem[]; + expected.forEach((item) => repository.add(item)); + + // When + const actual = [...repository.list()]; + + // Then + assert.deepEqual(actual, expected); + }); + }); + + describe("remove", () => { + it("should remove known", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + repository.add(item); + + // When + const didRemove = repository.remove(item.id); + + // Then + assert(didRemove); + assert.equal([...repository.list()].length, 0); + }); + + it("should ignore unknown", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + + // When + const didRemove = repository.remove(item.id); + + // Then + assert(!didRemove); + }); + }); + + describe("hasItem", () => { + it("should return true on known", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + repository.add(item); + + // When + const result = repository.hasItem(item); + + // Then + assert(result); + }); + + it("should return false on unknown", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + + // When + const result = repository.hasItem(item); + + // Then + assert(!result); + }); + }); + + describe("removeItem", () => { + it("should remove known", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + repository.add(item); + + // When + const didRemove = repository.removeItem(item); + + // Then + assert(didRemove); + assert.equal([...repository.list()].length, 0); + }); + + it("should ignore unknown", () => { + // Given + const repository = makeRepository(); + const item = { id: 0, value: "foo" } as TestItem; + + // When + const didRemove = repository.removeItem(item); + + // Then + assert(!didRemove); + }); + }); +}); diff --git a/test/spec/utils.test.mjs b/test/spec/utils.test.mjs deleted file mode 100644 index a9db373..0000000 --- a/test/spec/utils.test.mjs +++ /dev/null @@ -1,192 +0,0 @@ -import { describe, it, before, after } from 'node:test' -import assert from 'node:assert' -import sinon from 'sinon' -import { Timeout, combine, formatByteSize, formatDuration, memoize, range, sleep, withTimeout } from '../../src/utils.mjs' - -describe('utils', () => { - describe('memoized', () => { - it('should not call again with same params', () => { - // Given - const expected = 4 - const fn = sinon.mock() - fn.returns(expected) - - const mfn = memoize(fn) - - // When - mfn(16) - const actual = mfn(16) - - // Then - assert.equal(actual, expected) - assert(fn.calledOnce) - assert(fn.calledOnceWith(16)) - }) - - it('should call through on unknown', () => { - // Given - const fn = sinon.mock() - fn.twice().returns() - const mfn = memoize(fn) - - // When - mfn(16) - mfn(32) - - // Then - assert(fn.calledTwice) - assert(fn.calledWith(16)) - assert(fn.calledWith(32)) - }) - }) - - describe('withTimeout', () => { - /** @type {sinon.SinonFakeTimers} */ - let clock - - before(() => { - clock = sinon.useFakeTimers() - }) - - it('should return on resolve', async () => { - // Given - const expected = 42 - const promise = Promise.resolve(expected) - - // When - const actual = await withTimeout(promise, 8) - - // Then - assert.equal(actual, expected) - }) - - it('should throw on reject', () => { - // Given - const promise = Promise.reject(new Error()) - - // When + Then - assert.rejects( - () => withTimeout(promise, 8) - ) - }) - - it('should return symbol on timeout', async () => { - // Given - const expected = Timeout - const promise = sleep(16) - - // When - const actual = withTimeout(promise, 8) - - // Then - clock.tick(16100) - assert.equal(await actual, expected) - }) - - after(() => { - clock.restore() - }) - }) - - describe('range', () => { - it('should return numbers', () => { - // Given - const expected = [0, 1, 2, 3] - - // When - const actual = range(4) - - // Then - assert.deepEqual(actual, expected) - }) - - it('should return empty on 0', () => { - // Given - const expected = [] - - // When - const actual = range(0) - - // Then - assert.deepEqual(actual, expected) - }) - - it('should return empty on negative', () => { - // Given - const expected = [] - - // When - const actual = range(-4) - - // Then - assert.deepEqual(actual, expected) - }) - }) - - describe('combine', () => { - it('should return expected', () => { - // Given - const arrays = [ - ['a', 'b'], - [0, 1], - ['foo', 'bar'] - ] - - const expected = [ - ['a', 0, 'foo'], - ['a', 0, 'bar'], - ['a', 1, 'foo'], - ['a', 1, 'bar'], - ['b', 0, 'foo'], - ['b', 0, 'bar'], - ['b', 1, 'foo'], - ['b', 1, 'bar'], - ] - - // When - const actual = combine(...arrays) - - // Then - // Compare sorted, since order doesn't matter - assert.deepEqual(actual.sort(), expected.sort()) - }) - }) - - describe('formatByteSize', () => { - const cases = [ - [128 * Math.pow(1024, 0), '128b'], - [128 * Math.pow(1024, 1), '128kb'], - [128 * Math.pow(1024, 2), '128Mb'], - [128 * Math.pow(1024, 3), '128Gb'], - [128 * Math.pow(1024, 4), '128Tb'], - [128 * Math.pow(1024, 5), '128Pb'], - [128 * Math.pow(1024, 6), '128Eb'], - [128 * Math.pow(1024, 7), '128Zb'], - [128 * Math.pow(1024, 8), '128Yb'], - [8 * Math.pow(1024, 9), '8192Yb'], - ] - - cases.forEach(([input, expected]) => it(`should format ${expected}`, () => - assert.equal(formatByteSize(input), expected) - )) - }) - - describe('formatDuration', () => { - const cases = [ - [0.0000002, '0.2us'], - [0.000002, '2us'], - [0.002, '2ms'], - [2, '2sec'], - [120, '2min'], - [7200, '2hr'], - [172800, '2day'], - [1814400, '3wk'], - [10368000, '4mo'], - [378432000, '12yr'] - ] - - cases.forEach(([input, expected]) => it(`should format ${expected}`, () => - assert.equal(formatDuration(input), expected) - )) - }) -}) diff --git a/test/spec/utils.test.ts b/test/spec/utils.test.ts new file mode 100644 index 0000000..8683dfa --- /dev/null +++ b/test/spec/utils.test.ts @@ -0,0 +1,221 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import sinon from "sinon"; +import { + Timeout, + combine, + formatBandwidth, + formatByteSize, + formatDuration, + memoize, + range, + sleep, + withTimeout, +} from "../../src/utils.ts"; + +describe("utils", () => { + describe("memoized", () => { + it("should not call again with same params", () => { + // Given + const expected = 4; + const fn = sinon.mock(); + fn.returns(expected); + + const mfn = memoize(fn); + + // When + mfn(16); + const actual = mfn(16); + + // Then + assert.equal(actual, expected); + assert(fn.calledOnce); + assert(fn.calledOnceWith(16)); + }); + + it("should call through on unknown", () => { + // Given + const fn = sinon.mock(); + fn.twice().returns(undefined); + const mfn = memoize(fn); + + // When + mfn(16); + mfn(32); + + // Then + assert(fn.calledTwice); + assert(fn.calledWith(16)); + assert(fn.calledWith(32)); + }); + }); + + describe("withTimeout", () => { + let clock: sinon.SinonFakeTimers; + + before(() => { + clock = sinon.useFakeTimers(); + }); + + it("should return on resolve", async () => { + // Given + const expected = 42; + const promise = Promise.resolve(expected); + + // When + const actual = await withTimeout(promise, 8); + + // Then + assert.equal(actual, expected); + }); + + it("should throw on reject", () => { + // Given + const promise = Promise.reject(new Error()); + + // When + Then + assert.rejects(() => withTimeout(promise, 8)); + }); + + it("should return symbol on timeout", async () => { + // Given + const expected = Timeout; + const promise = sleep(16); + + // When + const actual = withTimeout(promise, 8); + + // Then + clock.tick(16100); + assert.equal(await actual, expected); + }); + + after(() => { + clock.restore(); + }); + }); + + describe("range", () => { + it("should return numbers", () => { + // Given + const expected = [0, 1, 2, 3]; + + // When + const actual = range(4); + + // Then + assert.deepEqual(actual, expected); + }); + + it("should return empty on 0", () => { + // Given + const expected = [] as number[]; + + // When + const actual = range(0); + + // Then + assert.deepEqual(actual, expected); + }); + + it("should return empty on negative", () => { + // Given + const expected = [] as number[]; + + // When + const actual = range(-4); + + // Then + assert.deepEqual(actual, expected); + }); + }); + + describe("combine", () => { + it("should return expected", () => { + // Given + const arrays = [ + ["a", "b"], + [0, 1], + ["foo", "bar"], + ]; + + const expected = [ + ["a", 0, "foo"], + ["a", 0, "bar"], + ["a", 1, "foo"], + ["a", 1, "bar"], + ["b", 0, "foo"], + ["b", 0, "bar"], + ["b", 1, "foo"], + ["b", 1, "bar"], + ]; + + // When + const actual = combine(...arrays); + + // Then + // Compare sorted, since order doesn't matter + assert.deepEqual(actual.sort(), expected.sort()); + }); + }); + + describe("formatByteSize", () => { + const cases = [ + [128 * Math.pow(1024, 0), "128b"], + [128 * Math.pow(1024, 1), "128kb"], + [128 * Math.pow(1024, 2), "128Mb"], + [128 * Math.pow(1024, 3), "128Gb"], + [128 * Math.pow(1024, 4), "128Tb"], + [128 * Math.pow(1024, 5), "128Pb"], + [128 * Math.pow(1024, 6), "128Eb"], + [128 * Math.pow(1024, 7), "128Zb"], + [128 * Math.pow(1024, 8), "128Yb"], + [8 * Math.pow(1024, 9), "8192Yb"], + ] as Array<[number, string]>; + + cases.forEach(([input, expected]) => + it(`should format ${expected}`, () => + assert.equal(formatByteSize(input), expected)), + ); + }); + + describe("formatBandwidth", () => { + const cases = [ + [128 * Math.pow(1024, 0), "128bps"], + [128 * Math.pow(1024, 1), "128kbps"], + [128 * Math.pow(1024, 2), "128Mbps"], + [128 * Math.pow(1024, 3), "128Gbps"], + [128 * Math.pow(1024, 4), "128Tbps"], + [128 * Math.pow(1024, 5), "128Pbps"], + [128 * Math.pow(1024, 6), "128Ebps"], + [128 * Math.pow(1024, 7), "128Zbps"], + [128 * Math.pow(1024, 8), "128Ybps"], + [8 * Math.pow(1024, 9), "8192Ybps"], + ] as Array<[number, string]>; + + cases.forEach(([input, expected]) => + it(`should format ${expected}`, () => + assert.equal(formatBandwidth(input), expected)), + ); + }); + + describe("formatDuration", () => { + const cases = [ + [0.0000002, "0.2us"], + [0.000002, "2us"], + [0.002, "2ms"], + [2, "2sec"], + [120, "2min"], + [7200, "2hr"], + [172800, "2day"], + [1814400, "3wk"], + [10368000, "4mo"], + [378432000, "12yr"], + ] as Array<[number, string]>; + + cases.forEach(([input, expected]) => + it(`should format ${expected}`, () => + assert.equal(formatDuration(input), expected)), + ); + }); +}); From c8a928c91dfb72ede7efcf2312a83d918c5a2e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 12:29:51 +0200 Subject: [PATCH 14/22] make specs run --- src/relay/udp.relay.handler.ts | 6 +++--- test/spec/relay/dynamic.relaying.test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/relay/udp.relay.handler.ts b/src/relay/udp.relay.handler.ts index 84b2c49..c82e3db 100644 --- a/src/relay/udp.relay.handler.ts +++ b/src/relay/udp.relay.handler.ts @@ -55,10 +55,10 @@ export interface UDPRelayHandlerOptions { */ export class UDPRelayHandler extends EventEmitter { /** - * Socketp pool used for relays. + * Socket pool used for relays. */ - public readonly socketPool: UDPSocketPool; - + // TODO: Only public for testing, make `public readonly` + public socketPool: UDPSocketPool; private _relayTable: RelayEntry[] = []; /** diff --git a/test/spec/relay/dynamic.relaying.test.ts b/test/spec/relay/dynamic.relaying.test.ts index 20cd572..78a3136 100644 --- a/test/spec/relay/dynamic.relaying.test.ts +++ b/test/spec/relay/dynamic.relaying.test.ts @@ -23,7 +23,7 @@ describe("DynamicRelaying", () => { const relayHandler = sinon.createStubInstance(UDPRelayHandler); relayHandler.on.callThrough(); relayHandler.emit.callThrough(); - sinon.stub(relayHandler, "socketPool").value(socketPool); + relayHandler.socketPool = socketPool; relayHandler.createRelay.resolves(true); useDynamicRelay(relayHandler); @@ -55,7 +55,7 @@ describe("DynamicRelaying", () => { ); clock.restore(); await sleep(0.05); // Wait for relay to be created - clock = sinon.useFakeTimers(); + // clock = sinon.useFakeTimers(); // Then const createdRelay = relayHandler.createRelay.lastCall.args[0]; @@ -107,7 +107,7 @@ describe("DynamicRelaying", () => { ); clock.restore(); await sleep(0.05); // Wait for relay to be created - clock = sinon.useFakeTimers(); + // clock = sinon.useFakeTimers(); // Then assert(relayHandler.createRelay.notCalled); @@ -146,7 +146,7 @@ describe("DynamicRelaying", () => { ); clock.restore(); await sleep(0.05); // Wait for relay to be created - clock = sinon.useFakeTimers(); + // clock = sinon.useFakeTimers(); // Then assert(relayHandler.createRelay.notCalled); From cce40f8767ac7e4ca35ce3eab7e09ab40c372d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 12:47:23 +0200 Subject: [PATCH 15/22] port e2e --- package.json | 4 +- src/relay/net.address.ts | 3 +- src/relay/udp.relay.handler.ts | 2 +- test/e2e/connect.relay.test.mjs | 126 ----------------------------- test/e2e/connect.relay.test.ts | 137 ++++++++++++++++++++++++++++++++ test/e2e/connection.test.mjs | 52 ------------ test/e2e/connection.test.ts | 52 ++++++++++++ test/e2e/context.mjs | 118 --------------------------- test/e2e/context.ts | 125 +++++++++++++++++++++++++++++ test/e2e/host.test.mjs | 30 ------- test/e2e/host.test.ts | 36 +++++++++ test/e2e/is.ci.mjs | 6 -- test/e2e/is.ci.ts | 6 ++ test/e2e/udp.relay.test.mjs | 97 ---------------------- test/e2e/udp.relay.test.ts | 91 +++++++++++++++++++++ 15 files changed, 452 insertions(+), 433 deletions(-) delete mode 100644 test/e2e/connect.relay.test.mjs create mode 100644 test/e2e/connect.relay.test.ts delete mode 100644 test/e2e/connection.test.mjs create mode 100644 test/e2e/connection.test.ts delete mode 100644 test/e2e/context.mjs create mode 100644 test/e2e/context.ts delete mode 100644 test/e2e/host.test.mjs create mode 100644 test/e2e/host.test.ts delete mode 100644 test/e2e/is.ci.mjs create mode 100644 test/e2e/is.ci.ts delete mode 100644 test/e2e/udp.relay.test.mjs create mode 100644 test/e2e/udp.relay.test.ts diff --git a/package.json b/package.json index 7144c56..9447673 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "doc": "jsdoc -c .jsdoc.js src", "test": "node --test test/spec/**/*.test.ts", "test:e2e": "node --test test/e2e/**/*.test.ts | node scripts/taplog.mjs utap \"pino-pretty -c\"", - "start": "node bin/noray.mjs | pino-pretty", - "start:prod": "NODE_ENV=production node bin/noray.mjs" + "start": "node bin/noray.ts | pino-pretty", + "start:prod": "NODE_ENV=production node bin/noray.ts" }, "keywords": [ "online", diff --git a/src/relay/net.address.ts b/src/relay/net.address.ts index 9b9cd07..e0db429 100644 --- a/src/relay/net.address.ts +++ b/src/relay/net.address.ts @@ -1,4 +1,5 @@ import dgram from "node:dgram"; +import net from "node:net"; export interface NetAddressData { address: string; @@ -22,7 +23,7 @@ export class NetAddress implements NetAddressData { return `${this.address}:${this.port}`; } - static fromRinfo(rinfo: dgram.RemoteInfo): NetAddress { + static fromRinfo(rinfo: dgram.RemoteInfo | net.AddressInfo): NetAddress { return new NetAddress({ address: rinfo.address, port: rinfo.port, diff --git a/src/relay/udp.relay.handler.ts b/src/relay/udp.relay.handler.ts index c82e3db..70b4291 100644 --- a/src/relay/udp.relay.handler.ts +++ b/src/relay/udp.relay.handler.ts @@ -70,7 +70,7 @@ export class UDPRelayHandler extends EventEmitter { return this._relayTable; } - constructor(options: UDPRelayHandlerOptions) { + constructor(options?: UDPRelayHandlerOptions) { super(); this.socketPool = options?.socketPool ?? new UDPSocketPool(); diff --git a/test/e2e/connect.relay.test.mjs b/test/e2e/connect.relay.test.mjs deleted file mode 100644 index ab58b1e..0000000 --- a/test/e2e/connect.relay.test.mjs +++ /dev/null @@ -1,126 +0,0 @@ -import * as net from 'node:net' -import * as dgram from 'node:dgram' -import { describe, it, before, after } from 'node:test' -import assert, { fail } from 'node:assert' -import { End2EndContext } from './context.mjs' -import { promiseEvent, sleep } from '../../src/utils.mjs' - -describe('Connection', () => { - const context = new End2EndContext() - - const host = { - /** @type {net.Socket} */ - tcp: undefined, - - /** @type {dgram.Socket} */ - udp: undefined, - - oid: '', - pid: '' - } - - const client = { - /** @type {net.Socket} */ - tcp: undefined, - - /** @type {dgram.Socket} */ - udp: undefined, - - targetRelay: undefined, - - oid: '', - pid: '' - } - - before(async () => { - await context.startup() - - context.log.info('Connecting to noray') - host.tcp = await context.connect() - client.tcp = await context.connect() - - context.log.info('Creating UDP sockets') - host.udp = dgram.createSocket('udp4') - client.udp = dgram.createSocket('udp4') - - context.log.info('Binding UDP') - host.udp.bind() - client.udp.bind() - - context.log.info('Waiting for UDP sockets to start listening') - await Promise.all([ - promiseEvent(host.udp, 'listening'), - promiseEvent(client.udp, 'listening') - ]) - - context.log.info('Host bound to UDP port %d', host.udp.address().port) - context.log.info('Client bound to UDP port %d', client.udp.address().port) - - context.log.info('Startup done') - }) - - it('should register host', async () => { - // Register hosts - [host.oid, host.pid] = await context.registerHost(host.tcp) - assert(host.oid, 'Failed to get host oid!') - assert(host.pid, 'Failed to get host pid!') - - ;[client.oid, client.pid] = await context.registerHost(client.tcp) - assert(client.oid, 'Failed to get client oid!') - assert(client.pid, 'Failed to get client pid!') - - // Register UDP port - context.log.info('Registering host external address') - await context.registerExternal(host.udp, host.pid) - context.log.info('Registering client external address') - await context.registerExternal(client.udp, client.pid) - }) - - it('should reply with relay port', async () => { - // Request to connect - context.log.info('Connecting over relay') - client.tcp.write(`connect-relay ${host.oid}\n`) - - client.targetRelay = (await context.read(client.tcp)) - .filter(cmd => cmd.startsWith('connect-relay')) - .map(cmd => cmd.split(' ')[1]) - .at(0) ?? fail('Failed to get relay port!') - - context.log.info('Client received relay port %d', client.targetRelay) - }) - - it('should relay data', async () => { - // Since it's all running on localhost, let's assume the data gets through - const message = 'Hello from client!' - const response = 'Hello from host!' - - const hostReceived = [] - const clientReceived = [] - - host.udp.on('message', (msg, rinfo) => { - hostReceived.push(msg.toString()) - console.log('Host got message', JSON.stringify({ msg: msg.toString(), rinfo })) - host.udp.send(response, rinfo.port, rinfo.address) - }) - - client.udp.on('message', (msg, _rinfo) => { - clientReceived.push(msg.toString()) - }) - - console.log('Sending initial packet to port ', client.targetRelay) - client.udp.send(message, client.targetRelay) - - // Check if data went through both ways - context.log.info('Waiting for messages to go through') - await sleep(0.1) - assert.equal(hostReceived.join(''), message) - assert.equal(clientReceived.join(''), response) - }) - - after(() => { - host.udp.close() - client.udp.close() - - context.shutdown() - }) -}) diff --git a/test/e2e/connect.relay.test.ts b/test/e2e/connect.relay.test.ts new file mode 100644 index 0000000..e08c3bb --- /dev/null +++ b/test/e2e/connect.relay.test.ts @@ -0,0 +1,137 @@ +import * as net from "node:net"; +import * as dgram from "node:dgram"; +import { describe, it, before, after } from "node:test"; +import assert, { fail } from "node:assert"; +import { End2EndContext } from "./context.ts"; +import { promiseEvent, sleep } from "../../src/utils.ts"; + +interface HostInfo { + tcp: net.Socket | undefined; + udp: dgram.Socket | undefined; + + oid: string; + pid: string; +} + +interface ClientInfo extends HostInfo { + targetRelay: number | undefined; +} + +describe("Connection", () => { + const context = new End2EndContext(); + + const host = { + tcp: undefined, + udp: undefined, + + oid: "", + pid: "", + } as HostInfo; + + const client = { + tcp: undefined, + udp: undefined, + + targetRelay: undefined, + + oid: "", + pid: "", + } as ClientInfo; + + before(async () => { + await context.startup(); + + context.log.info("Connecting to noray"); + host.tcp = await context.connect(); + client.tcp = await context.connect(); + + context.log.info("Creating UDP sockets"); + host.udp = dgram.createSocket("udp4"); + client.udp = dgram.createSocket("udp4"); + + context.log.info("Binding UDP"); + host.udp.bind(); + client.udp.bind(); + + context.log.info("Waiting for UDP sockets to start listening"); + await Promise.all([ + promiseEvent(host.udp, "listening"), + promiseEvent(client.udp, "listening"), + ]); + + context.log.info("Host bound to UDP port %d", host.udp.address().port); + context.log.info("Client bound to UDP port %d", client.udp.address().port); + + context.log.info("Startup done"); + }); + + it("should register host", async () => { + // Register hosts + [host.oid, host.pid] = await context.registerHost(host.tcp!!); + assert(host.oid, "Failed to get host oid!"); + assert(host.pid, "Failed to get host pid!"); + + [client.oid, client.pid] = await context.registerHost(client.tcp!!); + assert(client.oid, "Failed to get client oid!"); + assert(client.pid, "Failed to get client pid!"); + + // Register UDP port + context.log.info("Registering host external address"); + await context.registerExternal(host.udp, host.pid); + context.log.info("Registering client external address"); + await context.registerExternal(client.udp, client.pid); + }); + + it("should reply with relay port", async () => { + // Request to connect + context.log.info("Connecting over relay"); + client.tcp!!.write(`connect-relay ${host.oid}\n`); + + client.targetRelay = + (await context.read(client.tcp!!)) + .filter((cmd) => cmd.startsWith("connect-relay")) + .map((cmd) => cmd.split(" ")[1]) + .map((it) => parseInt(it)) + .at(0) ?? fail("Failed to get relay port!"); + + context.log.info("Client received relay port %d", client.targetRelay); + }); + + it("should relay data", async () => { + // Since it's all running on localhost, let's assume the data gets through + const message = "Hello from client!"; + const response = "Hello from host!"; + + const hostReceived = [] as string[]; + const clientReceived = [] as string[]; + + host.udp!!.on("message", (msg, rinfo) => { + hostReceived.push(msg.toString()); + console.log( + "Host got message", + JSON.stringify({ msg: msg.toString(), rinfo }), + ); + host.udp!!.send(response, rinfo.port, rinfo.address); + }); + + client.udp!!.on("message", (msg, _rinfo) => { + clientReceived.push(msg.toString()); + }); + + console.log("Sending initial packet to port ", client.targetRelay); + client.udp!!.send(message, client.targetRelay!!); + + // Check if data went through both ways + context.log.info("Waiting for messages to go through"); + await sleep(0.1); + assert.equal(hostReceived.join(""), message); + assert.equal(clientReceived.join(""), response); + }); + + after(() => { + host.udp!!.close(); + client.udp!!.close(); + + context.shutdown(); + }); +}); diff --git a/test/e2e/connection.test.mjs b/test/e2e/connection.test.mjs deleted file mode 100644 index 0cb5dda..0000000 --- a/test/e2e/connection.test.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it, before, after } from 'node:test' -import assert from 'node:assert' -import { End2EndContext } from './context.mjs' - -describe('Connection', () => { - const context = new End2EndContext() - - before(async () => { - await context.startup() - }) - - describe('connect', () => { - it('should respond with external address', async () => { - const host = await context.connect() - const client = await context.connect() - - // Grab data from responses - context.log.info('Registering parties') - const [oid, pid] = await context.registerHost(host) - const [_, clientPid] = await context.registerHost(client) - - assert(oid, 'No oid received!') - assert(pid, 'No pid received!') - assert(clientPid, 'No client pid received!') - - // Register external addresses - context.log.info('Registering external addresses') - await Promise.all([ - context.registerExternal(undefined, pid), - context.registerExternal(undefined, clientPid) - ]) - - // Send connect request - client.write(`connect ${oid}\n`) - - // Assert responses - assert( - (await context.read(client)).find(cmd => cmd.startsWith('connect ')), - 'No handshake received by client!' - ) - - assert( - (await context.read(host)).find(cmd => cmd.startsWith('connect ')), - 'No handshake received by host!' - ) - }) - }) - - after(() => { - context.shutdown() - }) -}) diff --git a/test/e2e/connection.test.ts b/test/e2e/connection.test.ts new file mode 100644 index 0000000..eaf623b --- /dev/null +++ b/test/e2e/connection.test.ts @@ -0,0 +1,52 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import { End2EndContext } from "./context.ts"; + +describe("Connection", () => { + const context = new End2EndContext(); + + before(async () => { + await context.startup(); + }); + + describe("connect", () => { + it("should respond with external address", async () => { + const host = await context.connect(); + const client = await context.connect(); + + // Grab data from responses + context.log.info("Registering parties"); + const [oid, pid] = await context.registerHost(host); + const [_, clientPid] = await context.registerHost(client); + + assert(oid, "No oid received!"); + assert(pid, "No pid received!"); + assert(clientPid, "No client pid received!"); + + // Register external addresses + context.log.info("Registering external addresses"); + await Promise.all([ + context.registerExternal(undefined, pid), + context.registerExternal(undefined, clientPid), + ]); + + // Send connect request + client.write(`connect ${oid}\n`); + + // Assert responses + assert( + (await context.read(client)).find((cmd) => cmd.startsWith("connect ")), + "No handshake received by client!", + ); + + assert( + (await context.read(host)).find((cmd) => cmd.startsWith("connect ")), + "No handshake received by host!", + ); + }); + }); + + after(() => { + context.shutdown(); + }); +}); diff --git a/test/e2e/context.mjs b/test/e2e/context.mjs deleted file mode 100644 index 9a14aff..0000000 --- a/test/e2e/context.mjs +++ /dev/null @@ -1,118 +0,0 @@ -import * as net from 'node:net' -import * as dgram from 'node:dgram' -import logger from '../../src/logger.mjs' -import { Noray } from '../../src/noray.mjs' -import { promiseEvent, sleep } from '../../src/utils.mjs' -import { config } from '../../src/config.mjs' - -const READ_WAIT = 0.05 - -export class End2EndContext { - #clients = [] - - /** @type {Noray} */ - noray - - log = logger.child({ name: 'test' }) - - async startup () { - this.log.info('Starting app') - - this.noray = new Noray() - await this.noray.start() - - this.log.info('Startup done, ready for testing') - } - - async connect () { - const socket = net.createConnection({ - host: config.socket.host, - port: config.socket.port - }) - socket.setEncoding('utf8') - - await promiseEvent(socket, 'connect') - this.#clients.push(socket) - return socket - } - - async read (socket) { - await sleep(READ_WAIT) - - const lines = [] - for (let line = ''; line != null; line = socket.read()) { - lines.push(line) - } - - const result = lines.join('').split('\n') - this.log.debug({ result }, "Read data from noray") - return result - } - - /** - * @param {dgram.Socket} udp - * @param {string} pid - */ - async registerExternal (udp, pid) { - let done = false - let error - const throwaway = udp === undefined - - if (udp === undefined) { - udp = dgram.createSocket('udp4') - udp.bind() - await promiseEvent(udp, 'listening') - } - - udp.once('message', (buf, _rinfo) => { - const msg = buf.toString('utf8') - done = true - error = msg !== 'OK' && msg - }) - - for (let i = 0; i < 128 && !done; ++i) { - udp.send(pid, config.udpRelay.registrarPort) - this.log.debug('Sending remote registrar attempt #%d', i+1) - await sleep(0.1) - } - - if (!done) { - throw new Error('Registrar timed out!') - } else if (error) { - throw new Error(error) - } - - const result = udp.address().port - if (throwaway) { - udp.close() - } - - return result - } - - async registerHost (socket) { - socket.write('register-host\n') - - const data = await this.read(socket) - - const oid = data - .filter(cmd => cmd.startsWith('set-oid ')) - .map(cmd => cmd.split(' ')[1]) - .at(0) - - const pid = data - .filter(cmd => cmd.startsWith('set-pid ')) - .map(cmd => cmd.split(' ')[1]) - .at(0) - - return [oid, pid] - } - - shutdown () { - this.log.info('Closing %d connections', this.#clients.length) - this.#clients.forEach(c => c.destroy()) - - this.log.info('Terminating Noray') - this.noray.shutdown() - } -} diff --git a/test/e2e/context.ts b/test/e2e/context.ts new file mode 100644 index 0000000..5695581 --- /dev/null +++ b/test/e2e/context.ts @@ -0,0 +1,125 @@ +import * as net from "node:net"; +import * as dgram from "node:dgram"; +import logger from "../../src/logger.ts"; +import { Noray } from "../../src/noray.ts"; +import { promiseEvent, sleep } from "../../src/utils.ts"; +import { config } from "../../src/config.ts"; + +const READ_WAIT = 0.05; + +export class End2EndContext { + private clients: net.Socket[] = []; + + noray!: Noray; + log = logger.child({ name: "test" }); + + async startup(): Promise { + this.log.info("Starting app"); + + this.noray = new Noray(); + await this.noray.start(); + + this.log.info("Startup done, ready for testing"); + } + + async connect(): Promise { + const socket = net.createConnection({ + host: config.socket.host, + port: config.socket.port, + }); + socket.setEncoding("utf8"); + + await promiseEvent(socket, "connect"); + this.clients.push(socket); + return socket; + } + + async read(socket: net.Socket): Promise { + await sleep(READ_WAIT); + + const lines = []; + for (let line = ""; line != null; line = socket.read()) { + lines.push(line); + } + + const result = lines.join("").split("\n"); + this.log.debug({ result }, "Read data from noray"); + return result; + } + + /** + * Register external port with the remote registrar + * + * @returns external port + */ + async registerExternal( + udp: dgram.Socket | undefined, + pid: string, + ): Promise { + let done = false; + let error; + const throwaway = udp === undefined; + + if (udp === undefined) { + udp = dgram.createSocket("udp4"); + udp.bind(); + await promiseEvent(udp, "listening"); + } + + udp.once("message", (buf, _rinfo) => { + const msg = buf.toString("utf8"); + done = true; + error = msg !== "OK" && msg; + }); + + for (let i = 0; i < 128 && !done; ++i) { + udp.send(pid, config.udpRelay.registrarPort); + this.log.debug("Sending remote registrar attempt #%d", i + 1); + await sleep(0.1); + } + + if (!done) { + throw new Error("Registrar timed out!"); + } else if (error) { + throw new Error(error); + } + + const result = udp.address().port; + if (throwaway) { + udp.close(); + } + + return result; + } + + /** + * Register host. + * + * @returns [OID, PID] tuple + */ + async registerHost(socket: net.Socket): Promise<[string, string]> { + socket.write("register-host\n"); + + const data = await this.read(socket); + + const oid = data + .filter((cmd) => cmd.startsWith("set-oid ")) + .map((cmd) => cmd.split(" ")[1]) + .at(0)!!; + + const pid = data + .filter((cmd) => cmd.startsWith("set-pid ")) + .map((cmd) => cmd.split(" ")[1]) + .at(0)!!; + + return [oid, pid]; + } + + shutdown() { + this.log.info("Closing %d connections", this.clients.length); + this.clients.forEach((c) => c.destroy()); + + this.log.info("Terminating Noray"); + this.noray.shutdown(); + } +} diff --git a/test/e2e/host.test.mjs b/test/e2e/host.test.mjs deleted file mode 100644 index ca8ab13..0000000 --- a/test/e2e/host.test.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, before, after } from 'node:test' -import assert from 'node:assert' -import { End2EndContext } from './context.mjs' - -describe('Hosts', () => { - const context = new End2EndContext() - - before(async () => { - await context.startup() - }) - - describe('register', () => { - it('should respond with oid/pid', async () => { - const client = await context.connect() - - client.write('register-host\n') - - // Read response - const response = await context.read(client) - - // Check if we got both id's - assert(response.find(cmd => cmd.startsWith('set-oid')), 'Missing open id!') - assert(response.find(cmd => cmd.startsWith('set-pid')), 'Missing private id!') - }) - }) - - after(() => { - context.shutdown() - }) -}) diff --git a/test/e2e/host.test.ts b/test/e2e/host.test.ts new file mode 100644 index 0000000..0ec16e7 --- /dev/null +++ b/test/e2e/host.test.ts @@ -0,0 +1,36 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import { End2EndContext } from "./context.ts"; + +describe("Hosts", () => { + const context = new End2EndContext(); + + before(async () => { + await context.startup(); + }); + + describe("register", () => { + it("should respond with oid/pid", async () => { + const client = await context.connect(); + + client.write("register-host\n"); + + // Read response + const response = await context.read(client); + + // Check if we got both id's + assert( + response.find((cmd) => cmd.startsWith("set-oid")), + "Missing open id!", + ); + assert( + response.find((cmd) => cmd.startsWith("set-pid")), + "Missing private id!", + ); + }); + }); + + after(() => { + context.shutdown(); + }); +}); diff --git a/test/e2e/is.ci.mjs b/test/e2e/is.ci.mjs deleted file mode 100644 index 2017fdc..0000000 --- a/test/e2e/is.ci.mjs +++ /dev/null @@ -1,6 +0,0 @@ -/** -* Return true if we're running in CI -*/ -export function isCI() { - return !!(process.env.CI || process.env.GITHUB_ACTIONS); -} diff --git a/test/e2e/is.ci.ts b/test/e2e/is.ci.ts new file mode 100644 index 0000000..c452d3f --- /dev/null +++ b/test/e2e/is.ci.ts @@ -0,0 +1,6 @@ +/** + * Return true if we're running in CI + */ +export function isCI(): boolean { + return !!(process.env.CI || process.env.GITHUB_ACTIONS); +} diff --git a/test/e2e/udp.relay.test.mjs b/test/e2e/udp.relay.test.mjs deleted file mode 100644 index c245d74..0000000 --- a/test/e2e/udp.relay.test.mjs +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, after, before } from 'node:test' -import assert from 'node:assert' -import { End2EndContext } from './context.mjs' -import dgram from 'node:dgram' -import { UDPRelayHandler } from '../../src/relay/udp.relay.handler.mjs' -import { RelayEntry } from '../../src/relay/relay.entry.mjs' -import { NetAddress } from '../../src/relay/net.address.mjs' -import { sleep } from '../../src/utils.mjs' -import { UDPSocketPool } from '../../src/relay/udp.socket.pool.mjs' - -describe('UDP Relay', { concurrency: false, skip: true }, async () => { - const context = new End2EndContext() - - /** @type {dgram.Socket} */ - let sendSocket - /** @type {dgram.Socket} */ - let recvSocket - /** @type {UDPRelayHandler} */ - let relayHandler - /** @type {UDPSocketPool} */ - let socketPool - - before(async () => { - await context.startup() - - sendSocket = dgram.createSocket('udp4') - recvSocket = dgram.createSocket('udp4') - relayHandler = new UDPRelayHandler() - socketPool = relayHandler.socketPool - }) - - it('should relay traffic', async () => { - // Given - const recvData = [] - const message = Buffer.from('Hello!', 'utf8') - - context.log.info('Creating test sockets') - - recvSocket.once('message', (msg, rinfo) => - recvData.push({ msg, rinfo }) - ) - - await Promise.all([bindSocket(sendSocket), bindSocket(recvSocket)]) - - context.log.info( - 'Allocated ports %d and %d', - sendSocket.address().port, recvSocket.address().port - ) - - context.log.info('Creating relay') - - await relayHandler.createRelay(new RelayEntry({ - address: NetAddress.fromRinfo(sendSocket.address()), - port: await socketPool.allocatePort() - })) - - await relayHandler.createRelay(new RelayEntry({ - address: NetAddress.fromRinfo(recvSocket.address()), - port: await socketPool.allocatePort() - })) - - context.log.info( - { table: relayHandler.relayTable }, - 'Updated relay table' - ) - - // When - sendSocket.send(message, relayHandler.relayTable[1].port, '127.0.0.1') - await sleep(0.25) - - // Then - context.log.info({ recvData }, 'Received data') - assert.deepEqual(recvData[0]?.msg, message) - assert.equal(recvData[0].rinfo.address, '127.0.0.1') - assert.equal(recvData[0].rinfo.port, relayHandler.relayTable[0].port) - }) - - after(() => { - sendSocket.close() - recvSocket.close() - relayHandler.clear() - - context.shutdown() - }) -}) - -/** -* Bind socket but with promise. -* @param {dgram.Socket} socket -* @returns {Promise} -*/ -function bindSocket (socket, port) { - return new Promise((resolve, reject) => { - socket.bind(port, '127.0.0.1', resolve) - socket.once('error', reject) - }) -} diff --git a/test/e2e/udp.relay.test.ts b/test/e2e/udp.relay.test.ts new file mode 100644 index 0000000..b2c1816 --- /dev/null +++ b/test/e2e/udp.relay.test.ts @@ -0,0 +1,91 @@ +import { describe, it, after, before } from "node:test"; +import assert from "node:assert"; +import { End2EndContext } from "./context.ts"; +import dgram from "node:dgram"; +import { UDPRelayHandler } from "../../src/relay/udp.relay.handler.ts"; +import { RelayEntry } from "../../src/relay/relay.entry.ts"; +import { NetAddress } from "../../src/relay/net.address.ts"; +import { sleep } from "../../src/utils.ts"; +import { UDPSocketPool } from "../../src/relay/udp.socket.pool.ts"; + +describe("UDP Relay", { concurrency: false, skip: true }, async () => { + const context = new End2EndContext(); + + let sendSocket: dgram.Socket; + let recvSocket: dgram.Socket; + let relayHandler: UDPRelayHandler; + let socketPool: UDPSocketPool; + + before(async () => { + await context.startup(); + + sendSocket = dgram.createSocket("udp4"); + recvSocket = dgram.createSocket("udp4"); + relayHandler = new UDPRelayHandler(); + socketPool = relayHandler.socketPool; + }); + + it("should relay traffic", async () => { + // Given + const recvData = [] as any[]; + const message = Buffer.from("Hello!", "utf8"); + + context.log.info("Creating test sockets"); + + recvSocket.once("message", (msg, rinfo) => recvData.push({ msg, rinfo })); + + await Promise.all([bindSocket(sendSocket), bindSocket(recvSocket)]); + + context.log.info( + "Allocated ports %d and %d", + sendSocket.address().port, + recvSocket.address().port, + ); + + context.log.info("Creating relay"); + + relayHandler.createRelay( + new RelayEntry({ + address: NetAddress.fromRinfo(sendSocket.address()), + port: await socketPool.allocatePort(), + }), + ); + + relayHandler.createRelay( + new RelayEntry({ + address: NetAddress.fromRinfo(recvSocket.address()), + port: await socketPool.allocatePort(), + }), + ); + + context.log.info({ table: relayHandler.relayTable }, "Updated relay table"); + + // When + sendSocket.send(message, relayHandler.relayTable[1].port, "127.0.0.1"); + await sleep(0.25); + + // Then + context.log.info({ recvData }, "Received data"); + assert.deepEqual(recvData[0]?.msg, message); + assert.equal(recvData[0].rinfo.address, "127.0.0.1"); + assert.equal(recvData[0].rinfo.port, relayHandler.relayTable[0].port); + }); + + after(() => { + sendSocket.close(); + recvSocket.close(); + relayHandler.clear(); + + context.shutdown(); + }); +}); + +/** + * Bind socket but with promise. + */ +function bindSocket(socket: dgram.Socket, port?: number): Promise { + return new Promise((resolve, reject) => { + socket.bind(port, "127.0.0.1", resolve); + socket.once("error", reject); + }); +} From e62f538649a63ca060d6f6173c38addb6420458a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 12:52:01 +0200 Subject: [PATCH 16/22] port taplog --- scripts/taplog.mjs | 51 ----------------------------------------- scripts/taplog.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 51 deletions(-) delete mode 100644 scripts/taplog.mjs create mode 100644 scripts/taplog.ts diff --git a/scripts/taplog.mjs b/scripts/taplog.mjs deleted file mode 100644 index 748c9e2..0000000 --- a/scripts/taplog.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import * as rl from 'node:readline/promises' -import * as chp from 'node:child_process' - -function sleep (t) { - return new Promise(resolve => setTimeout(resolve, t)) -} - -/** -* Run taplog -* @param {string} tapFormat Command for TAP formatting -* @param {string} logFormat Command for log formatting -*/ -async function taplog (tapFormat, logFormat) { - const reader = rl.createInterface({ - input: process.stdin - }) - - const tapLines = [] - const tap = chp.exec(tapFormat) - const log = chp.exec(logFormat) - - let testPass = true - - tap.stdout.pipe(process.stdout) - log.stdout.pipe(process.stdout) - - reader.on('line', line => { - const logRegex = /^\s*#*\s*{/ - const failRegex = /\s*not ok \d+/ - - if (logRegex.test(line)) { - log.stdin.write(line.replace(logRegex, '{') + '\n') - } else { - tapLines.push(line) - - testPass &&= !failRegex.test(line) - } - }) - - reader.on('close', async () => { - tapLines.forEach(line => tap.stdin.write(line + '\n')) - await sleep(25) - - tap.kill() - log.kill() - - process.exit(testPass ? 0 : 1) - }) -} - -taplog(process.argv[2], process.argv[3]) diff --git a/scripts/taplog.ts b/scripts/taplog.ts new file mode 100644 index 0000000..2a2f0fc --- /dev/null +++ b/scripts/taplog.ts @@ -0,0 +1,57 @@ +import * as rl from "node:readline/promises"; +import * as chp from "node:child_process"; +import { fail } from "node:assert"; + +function sleep(t: number): Promise { + return new Promise((resolve) => setTimeout(resolve, t)); +} + +/** + * Run taplog + * @param tapFormat Command for TAP formatting + * @param logFormat Command for log formatting + */ +async function taplog(tapFormat: string, logFormat: string) { + const reader = rl.createInterface({ + input: process.stdin, + }); + + const tapLines = [] as string[]; + const tap = chp.exec(tapFormat); + const log = chp.exec(logFormat); + + if (tap.stdout == null) fail("Tap process has no stdout!"); + if (log.stdout == null) fail("Log process has no stdout!"); + if (tap.stdin == null) fail("Tap process has no stdin!"); + if (log.stdin == null) fail("Log process has no stdin!"); + + let testPass = true; + + tap.stdout.pipe(process.stdout); + log.stdout.pipe(process.stdout); + + reader.on("line", (line) => { + const logRegex = /^\s*#*\s*{/; + const failRegex = /\s*not ok \d+/; + + if (logRegex.test(line)) { + log.stdin?.write(line.replace(logRegex, "{") + "\n"); + } else { + tapLines.push(line); + + testPass &&= !failRegex.test(line); + } + }); + + reader.on("close", async () => { + tapLines.forEach((line) => tap.stdin?.write(line + "\n")); + await sleep(25); + + tap.kill(); + log.kill(); + + process.exit(testPass ? 0 : 1); + }); +} + +taplog(process.argv[2], process.argv[3]); From 711971da0620b975016dc067761fdd1dbae9f500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 12:57:46 +0200 Subject: [PATCH 17/22] fxs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9447673..2a130ed 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint": "eslint --ext .ts src", "doc": "jsdoc -c .jsdoc.js src", "test": "node --test test/spec/**/*.test.ts", - "test:e2e": "node --test test/e2e/**/*.test.ts | node scripts/taplog.mjs utap \"pino-pretty -c\"", + "test:e2e": "node --test test/e2e/**/*.test.ts | node scripts/taplog.ts utap \"pino-pretty -c\"", "start": "node bin/noray.ts | pino-pretty", "start:prod": "NODE_ENV=production node bin/noray.ts" }, From bc2040319fcebe32f9f3c5429ae36c82de733e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 17:38:20 +0200 Subject: [PATCH 18/22] bv --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a130ed..fd6980a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@foxssake/noray", - "version": "1.5.2", + "version": "1.5.3", "description": "Online multiplayer orchestrator and potential game platform", "main": "src/noray.ts", "type": "module", From 864c47048205070918804021f2d8fd3f30235794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 17:57:24 +0200 Subject: [PATCH 19/22] lint --- .eslintrc.js | 15 - eslint.config.mjs | 14 + package.json | 7 +- pnpm-lock.yaml | 812 ++-- src/config.parsers.ts | 2 +- src/config.ts | 2 +- src/connection/connection.commands.ts | 8 +- src/metrics/metrics.registry.ts | 4 +- src/relay/bandwidth.limiter.ts | 4 +- src/relay/constraints.ts | 6 +- src/relay/relay.ts | 26 +- src/relay/udp.relay.handler.ts | 4 +- src/relay/udp.remote.registrar.ts | 7 +- src/relay/udp.socket.pool.ts | 4 +- src/utils.ts | 18 +- src/wordlist.ts | 5278 ++++++++++++------------- 16 files changed, 3152 insertions(+), 3059 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 eslint.config.mjs diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 5e77eae..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true - }, - extends: 'standard', - overrides: [ - ], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module' - }, - rules: { - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..dbffc12 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,14 @@ +// @ts-check + +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig({ + files: ["**/*.{js,ts}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + tseslint.configs.stylistic, + ], +}); diff --git a/package.json b/package.json index fd6980a..e5b78b3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "noray": "bin/noray.ts" }, "scripts": { - "lint": "eslint --ext .ts src", + "lint": "eslint src bin", "doc": "jsdoc -c .jsdoc.js src", "test": "node --test test/spec/**/*.test.ts", "test:e2e": "node --test test/e2e/**/*.test.ts | node scripts/taplog.ts utap \"pino-pretty -c\"", @@ -30,7 +30,8 @@ "url": "https://github.com/foxssake/noray/issues" }, "devDependencies": { - "eslint": "^8.36.0", + "@eslint/js": "^10.0.1", + "eslint": "^10.4.1", "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0", @@ -38,6 +39,8 @@ "jsdoc": "^4.0.2", "pino-pretty": "^10.0.0", "sinon": "^15.0.4", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", "utap": "^0.2.0" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7d7948..a61da6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,10 @@ importers: dependencies: '@foxssake/trimsock-js': specifier: ^0.17.0 - version: 0.17.0(typescript@5.9.2) + version: 0.17.0(typescript@6.0.3) '@foxssake/trimsock-node': specifier: ^0.17.0 - version: 0.17.0(typescript@5.9.2) + version: 0.17.0(typescript@6.0.3) dotenv: specifier: ^16.0.3 version: 16.5.0 @@ -27,21 +27,24 @@ importers: specifier: ^14.2.0 version: 14.2.0 devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.1) eslint: - specifier: ^8.36.0 - version: 8.57.1 + specifier: ^10.4.1 + version: 10.4.1 eslint-config-standard: specifier: ^17.0.0 - version: 17.1.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + version: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1))(eslint-plugin-n@15.7.0(eslint@10.4.1))(eslint-plugin-promise@6.6.0(eslint@10.4.1))(eslint@10.4.1) eslint-plugin-import: specifier: ^2.25.2 - version: 2.31.0(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1) eslint-plugin-n: specifier: ^15.0.0 - version: 15.7.0(eslint@8.57.1) + version: 15.7.0(eslint@10.4.1) eslint-plugin-promise: specifier: ^6.0.0 - version: 6.6.0(eslint@8.57.1) + version: 6.6.0(eslint@10.4.1) jsdoc: specifier: ^4.0.2 version: 4.0.4 @@ -54,6 +57,12 @@ importers: sinon: specifier: ^15.0.4 version: 15.2.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.60.1 + version: 8.60.1(eslint@10.4.1)(typescript@6.0.3) utap: specifier: ^0.2.0 version: 0.2.0 @@ -77,23 +86,44 @@ packages: resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} engines: {node: '>=6.9.0'} - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@foxssake/trimsock-js@0.17.0': resolution: {integrity: sha512-s81agKjOeUOZ/E+3SdjIntEnc7r879QbREBsVPmDvHuw2tDUdq7SsWJomNjwdFpt8Vlqj7WVTQ+pioo2T5De2A==} @@ -106,35 +136,30 @@ packages: peerDependencies: typescript: ^5.0.0 - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@jsdoc/salty@0.2.9': resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==} engines: {node: '>=v12.0.0'} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -156,6 +181,15 @@ packages: Deprecated: no longer maintained and no longer used by Sinon packages. See https://github.com/sinonjs/nise/issues/243 for replacement details. + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -168,9 +202,64 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - deprecated: Potential CWE-502 - Update to 1.3.1 or higher + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -181,21 +270,13 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -239,6 +320,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -251,6 +336,10 @@ packages: brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -269,29 +358,14 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - catharsis@0.9.0: resolution: {integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==} engines: {node: '>= 10'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.4.1: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -325,8 +399,8 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -353,10 +427,6 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -469,9 +539,9 @@ packages: peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-utils@2.1.0: resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} @@ -495,18 +565,26 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -548,31 +626,34 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -599,14 +680,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -618,9 +691,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -658,21 +728,14 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -737,10 +800,6 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -787,10 +846,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - js2xmlparser@4.0.2: resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} @@ -836,9 +891,6 @@ packages: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -864,6 +916,10 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1057,18 +1113,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1079,6 +1127,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pino-abstract-transport@1.2.0: resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} @@ -1123,9 +1175,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -1152,27 +1201,11 @@ packages: requizzle@0.2.4: resolution: {integrity: sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} hasBin: true - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -1204,6 +1237,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.2: + resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -1270,10 +1308,6 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -1293,12 +1327,19 @@ packages: tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thread-stream@2.7.0: resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -1314,10 +1355,6 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -1334,8 +1371,15 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -1407,65 +1451,68 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1)': dependencies: - eslint: 8.57.1 + eslint: 10.4.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/eslintrc@2.1.4': + '@eslint/config-array@0.23.5': dependencies: - ajv: 6.12.6 - debug: 4.4.1 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} - - '@foxssake/trimsock-js@0.17.0(typescript@5.9.2)': + '@eslint/config-helpers@0.6.0': dependencies: - typescript: 5.9.2 + '@eslint/core': 1.2.1 - '@foxssake/trimsock-node@0.17.0(typescript@5.9.2)': + '@eslint/core@1.2.1': dependencies: - '@foxssake/trimsock-js': 0.17.0(typescript@5.9.2) - typescript: 5.9.2 + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.4.1)': + optionalDependencies: + eslint: 10.4.1 + + '@eslint/object-schema@3.0.5': {} - '@humanwhocodes/config-array@0.13.0': + '@eslint/plugin-kit@0.7.2': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@eslint/core': 1.2.1 + levn: 0.4.1 - '@humanwhocodes/module-importer@1.0.1': {} + '@foxssake/trimsock-js@0.17.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 - '@humanwhocodes/object-schema@2.0.3': {} + '@foxssake/trimsock-node@0.17.0(typescript@6.0.3)': + dependencies: + '@foxssake/trimsock-js': 0.17.0(typescript@6.0.3) + typescript: 6.0.3 - '@jsdoc/salty@0.2.9': + '@humanfs/core@0.19.2': dependencies: - lodash: 4.17.21 + '@humanfs/types': 0.15.0 - '@nodelib/fs.scandir@2.1.5': + '@humanfs/node@0.16.8': dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 - '@nodelib/fs.stat@2.0.5': {} + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} - '@nodelib/fs.walk@1.2.8': + '@humanwhocodes/retry@0.4.3': {} + + '@jsdoc/salty@0.2.9': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + lodash: 4.17.21 '@rtsao/scc@1.1.0': {} @@ -1489,6 +1536,12 @@ snapshots: '@sinonjs/text-encoding@0.7.3': {} + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} '@types/linkify-it@5.0.0': {} @@ -1500,31 +1553,114 @@ snapshots: '@types/mdurl@2.0.0': {} - '@ungap/structured-clone@1.3.0': {} + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.1 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.60.1': {} + + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.2 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: - acorn: 8.14.1 + acorn: 8.16.0 - acorn@8.14.1: {} + acorn@8.16.0: {} - ajv@6.12.6: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - argparse@2.0.1: {} array-buffer-byte-length@1.0.2: @@ -1585,6 +1721,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} bintrees@1.0.2: {} @@ -1596,6 +1734,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -1622,25 +1764,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - catharsis@0.9.0: dependencies: lodash: 4.17.21 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.4.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - colorette@2.0.20: {} concat-map@0.0.1: {} @@ -1675,7 +1804,7 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -1699,10 +1828,6 @@ snapshots: dependencies: esutils: 2.0.3 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dotenv@16.5.0: {} dunder-proto@1.0.1: @@ -1803,12 +1928,12 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1))(eslint-plugin-n@15.7.0(eslint@10.4.1))(eslint-plugin-promise@6.6.0(eslint@10.4.1))(eslint@10.4.1): dependencies: - eslint: 8.57.1 - eslint-plugin-import: 2.31.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) + eslint: 10.4.1 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1) + eslint-plugin-n: 15.7.0(eslint@10.4.1) + eslint-plugin-promise: 6.6.0(eslint@10.4.1) eslint-import-resolver-node@0.3.9: dependencies: @@ -1818,22 +1943,23 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint@10.4.1): dependencies: debug: 3.2.7 optionalDependencies: - eslint: 8.57.1 + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + eslint: 10.4.1 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-es@4.1.0(eslint@8.57.1): + eslint-plugin-es@4.1.0(eslint@10.4.1): dependencies: - eslint: 8.57.1 + eslint: 10.4.1 eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.31.0(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -1842,9 +1968,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.57.1 + eslint: 10.4.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint@10.4.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -1855,29 +1981,33 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-n@15.7.0(eslint@8.57.1): + eslint-plugin-n@15.7.0(eslint@10.4.1): dependencies: builtins: 5.1.0 - eslint: 8.57.1 - eslint-plugin-es: 4.1.0(eslint@8.57.1) - eslint-utils: 3.0.0(eslint@8.57.1) + eslint: 10.4.1 + eslint-plugin-es: 4.1.0(eslint@10.4.1) + eslint-utils: 3.0.0(eslint@10.4.1) ignore: 5.3.2 is-core-module: 2.16.1 minimatch: 3.1.2 resolve: 1.22.10 semver: 7.7.2 - eslint-plugin-promise@6.6.0(eslint@8.57.1): + eslint-plugin-promise@6.6.0(eslint@10.4.1): dependencies: - eslint: 8.57.1 + eslint: 10.4.1 - eslint-scope@7.2.2: + eslint-scope@9.1.2: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 esrecurse: 4.3.0 estraverse: 5.3.0 @@ -1885,9 +2015,9 @@ snapshots: dependencies: eslint-visitor-keys: 1.3.0 - eslint-utils@3.0.0(eslint@8.57.1): + eslint-utils@3.0.0(eslint@10.4.1): dependencies: - eslint: 8.57.1 + eslint: 10.4.1 eslint-visitor-keys: 2.1.0 eslint-visitor-keys@1.3.0: {} @@ -1896,56 +2026,50 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint@8.57.1: + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.1: dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 - chalk: 4.1.2 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.1 - doctrine: 3.0.0 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@9.6.1: + espree@11.2.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 3.4.3 + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -1973,33 +2097,30 @@ snapshots: fast-safe-stringify@2.1.1: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 - rimraf: 3.0.2 - flatted@3.3.3: {} + flatted@3.4.2: {} for-each@0.3.5: dependencies: is-callable: 1.2.7 - fs.realpath@1.0.0: {} - function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -2041,19 +2162,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -2063,8 +2171,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -2093,20 +2199,10 @@ snapshots: ignore@5.3.2: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + ignore@7.0.5: {} imurmurhash@0.1.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -2179,8 +2275,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-path-inside@3.0.3: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -2226,10 +2320,6 @@ snapshots: joycon@3.1.1: {} - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - js2xmlparser@4.0.2: dependencies: xmlcreate: 2.0.4 @@ -2287,8 +2377,6 @@ snapshots: lodash.get@4.4.2: {} - lodash.merge@4.6.2: {} - lodash@4.17.21: {} markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0): @@ -2311,6 +2399,10 @@ snapshots: mdurl@2.0.0: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -2397,20 +2489,16 @@ snapshots: dependencies: p-limit: 3.1.0 - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} path-to-regexp@6.3.0: {} + picomatch@4.0.4: {} + pino-abstract-transport@1.2.0: dependencies: readable-stream: 4.7.0 @@ -2470,8 +2558,6 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - quick-format-unescaped@4.0.4: {} readable-stream@4.7.0: @@ -2510,24 +2596,12 @@ snapshots: dependencies: lodash: 4.17.21 - resolve-from@4.0.0: {} - resolve@1.22.10: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -2557,6 +2631,8 @@ snapshots: semver@7.7.2: {} + semver@7.8.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -2660,10 +2736,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} @@ -2678,12 +2750,19 @@ snapshots: dependencies: bintrees: 1.0.2 - text-table@0.2.0: {} - thread-stream@2.7.0: dependencies: real-require: 0.2.0 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -2699,8 +2778,6 @@ snapshots: type-detect@4.1.0: {} - type-fest@0.20.2: {} - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -2734,7 +2811,18 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@5.9.2: {} + typescript-eslint@8.60.1(eslint@10.4.1)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} uc.micro@2.1.0: {} diff --git a/src/config.parsers.ts b/src/config.parsers.ts index 9c83faa..b1371fe 100644 --- a/src/config.parsers.ts +++ b/src/config.parsers.ts @@ -140,7 +140,7 @@ export function ports(value: string | undefined): number[] | undefined { .filter((p) => p !== undefined) .map((p) => [p, p]); - const absolutes: Array<[number, number]> = ranges + const absolutes: [number, number][] = ranges .filter((r) => r.includes("-")) .map((r) => r.split("-")) .map(([from, to]) => [integer(from), integer(to)] as [number, number]) diff --git a/src/config.ts b/src/config.ts index a59b8a4..b42929d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,7 +10,7 @@ import { import logger, { getLogLevel } from "./logger.ts"; import { urlAlphabet } from "nanoid"; -type ConfigEnv = { [key: string]: string | undefined }; +type ConfigEnv = Record; export function readConfig(env: ConfigEnv) { return { diff --git a/src/connection/connection.commands.ts b/src/connection/connection.commands.ts index 1b34575..c7d4e83 100644 --- a/src/connection/connection.commands.ts +++ b/src/connection/connection.commands.ts @@ -59,8 +59,8 @@ export function handleConnectRelay(hostRepository: HostRepository) { assert(client, "Unknown client from address"); log.debug("Ensuring relay for both parties"); - host.relay = getRelay(host.rinfo!!); - client.relay = getRelay(client.rinfo!!); + host.relay = getRelay(host.rinfo!); + client.relay = getRelay(client.rinfo!); log.debug( { host: host.relay, client: client.relay }, @@ -68,11 +68,11 @@ export function handleConnectRelay(hostRepository: HostRepository) { ); server.send(socket, { name: "connect-relay", - params: [host.relay!!.toString()], + params: [host.relay!.toString()], }); server.send(host.socket, { name: "connect-relay", - params: [client.relay!!.toString()], + params: [client.relay!.toString()], }); log.debug( { diff --git a/src/metrics/metrics.registry.ts b/src/metrics/metrics.registry.ts index 8a8eb81..dee25ba 100644 --- a/src/metrics/metrics.registry.ts +++ b/src/metrics/metrics.registry.ts @@ -1,3 +1,3 @@ -import * as prometheus from "prom-client"; +import * as prometheus from 'prom-client' -export const metricsRegistry = new prometheus.Registry(); +export const metricsRegistry = new prometheus.Registry() diff --git a/src/relay/bandwidth.limiter.ts b/src/relay/bandwidth.limiter.ts index 1c9a668..b9438e8 100644 --- a/src/relay/bandwidth.limiter.ts +++ b/src/relay/bandwidth.limiter.ts @@ -27,10 +27,10 @@ export class BandwidthLimitExceededError extends Error { } * new one is started. */ export class BandwidthLimiter { - private interval: number = 1; + private interval = 1; private maxTraffic: number; - private traffic: number = 0; + private traffic = 0; private lastInterval: number = time(); /** diff --git a/src/relay/constraints.ts b/src/relay/constraints.ts index 8c875c0..62203d3 100644 --- a/src/relay/constraints.ts +++ b/src/relay/constraints.ts @@ -13,7 +13,7 @@ import { time } from "../utils.ts"; export function constrainIndividualBandwidth( relayHandler: UDPRelayHandler, traffic: number, - interval: number = 1, + interval = 1, ) { const limiters = new Map(); @@ -47,7 +47,7 @@ export function constrainIndividualBandwidth( export function constrainGlobalBandwidth( relayHandler: UDPRelayHandler, traffic: number, - interval: number = 1, + interval = 1, ) { const limiter = new BandwidthLimiter({ maxTraffic: traffic, @@ -68,7 +68,7 @@ export function constrainLifetime( relayHandler: UDPRelayHandler, duration: number, ) { - relayHandler.on("transmit", (source, _target, _message) => { + relayHandler.on("transmit", (source) => { // TODO: Prefer custom exception assert( time() - source.created < duration, diff --git a/src/relay/relay.ts b/src/relay/relay.ts index d0ae6c4..1569e19 100644 --- a/src/relay/relay.ts +++ b/src/relay/relay.ts @@ -27,11 +27,11 @@ const log = logger.child({ name: "mod:relay" }); Noray.hook(async (noray) => { log.info( "Starting periodic UDP relay cleanup job, running every %s", - formatDuration(config.udpRelay.cleanupInterval!!), + formatDuration(config.udpRelay.cleanupInterval!), ); const cleanupJob = setInterval( - () => cleanupUdpRelayTable(udpRelayHandler, config.udpRelay.timeout!!), - config.udpRelay.cleanupInterval!! * 1000, + () => cleanupUdpRelayTable(udpRelayHandler, config.udpRelay.timeout!), + config.udpRelay.cleanupInterval! * 1000, ); log.info( @@ -48,9 +48,9 @@ Noray.hook(async (noray) => { ) .catch((e) => log.error(e, "Remote registrar failed to listen!")); - log.info("Binding %d ports for relaying", config.udpRelay.ports!!.length); + log.info("Binding %d ports for relaying", config.udpRelay.ports!.length); - for (const port of config.udpRelay.ports!!) { + for (const port of config.udpRelay.ports!) { log.trace("Binding port %d for relay", port); try { await udpSocketPool.allocatePort(port); @@ -61,29 +61,29 @@ Noray.hook(async (noray) => { log.info( "Limiting relay bandwidth to %s/s and global bandwidth to %s/s", - formatByteSize(config.udpRelay.maxIndividualTraffic!!), - formatByteSize(config.udpRelay.maxGlobalTraffic!!), + formatByteSize(config.udpRelay.maxIndividualTraffic!), + formatByteSize(config.udpRelay.maxGlobalTraffic!), ); constrainIndividualBandwidth( udpRelayHandler, - config.udpRelay.maxIndividualTraffic!!, + config.udpRelay.maxIndividualTraffic!, config.udpRelay.trafficInterval, ); constrainGlobalBandwidth( udpRelayHandler, - config.udpRelay.maxGlobalTraffic!!, + config.udpRelay.maxGlobalTraffic!, config.udpRelay.trafficInterval, ); log.info( "Blocking relay traffic after %s or %s", - formatDuration(config.udpRelay.maxLifetimeDuration!!), - formatByteSize(config.udpRelay.maxLifetimeTraffic!!), + formatDuration(config.udpRelay.maxLifetimeDuration!), + formatByteSize(config.udpRelay.maxLifetimeTraffic!), ); - constrainLifetime(udpRelayHandler, config.udpRelay.maxLifetimeDuration!!); - constrainTraffic(udpRelayHandler, config.udpRelay.maxLifetimeTraffic!!); + constrainLifetime(udpRelayHandler, config.udpRelay.maxLifetimeDuration!); + constrainTraffic(udpRelayHandler, config.udpRelay.maxLifetimeTraffic!); log.info("Applying dynamic relaying"); useDynamicRelay(udpRelayHandler); diff --git a/src/relay/udp.relay.handler.ts b/src/relay/udp.relay.handler.ts index 70b4291..1159579 100644 --- a/src/relay/udp.relay.handler.ts +++ b/src/relay/udp.relay.handler.ts @@ -89,13 +89,13 @@ export class UDPRelayHandler extends EventEmitter { if (this.hasRelay(relay)) { // We already have this relay entry log.trace({ relay }, "Relay already exists, ignoring"); - return this._relayTable.find((e) => e.equals(relay))!!; + return this._relayTable.find((e) => e.equals(relay))!; } relay.port = this.socketPool.getPort(); this.emit("create", relay); - const socket = this.socketPool.getSocket(relay.port)!!; + const socket = this.socketPool.getSocket(relay.port)!; socket.removeAllListeners("message").on("message", (msg, rinfo) => { this.relay(msg, NetAddress.fromRinfo(rinfo), relay.port); }); diff --git a/src/relay/udp.remote.registrar.ts b/src/relay/udp.remote.registrar.ts index 23c3b43..db33ac9 100644 --- a/src/relay/udp.remote.registrar.ts +++ b/src/relay/udp.remote.registrar.ts @@ -57,7 +57,7 @@ export class UDPRemoteRegistrar { /** * Start listening for incoming requests. */ - listen(port: number = 0, address: string = "0.0.0.0"): Promise { + listen(port = 0, address = "0.0.0.0"): Promise { return new Promise((resolve) => { this.socket.on("message", (msg, rinfo) => this.handle(msg, rinfo)); this.socket.bind(port, address, () => { @@ -86,9 +86,10 @@ export class UDPRemoteRegistrar { host.rinfo = rinfo; this.socket.send("OK", rinfo.port, rinfo.address); registerSuccessCounter.inc(); - } catch (e: any) { + } catch (e) { registerFailCounter.inc(); - this.socket.send(e.message ?? "Error", rinfo.port, rinfo.address); + const message = e instanceof Error ? e.message : "Error"; + this.socket.send(message, rinfo.port, rinfo.address); } } } diff --git a/src/relay/udp.socket.pool.ts b/src/relay/udp.socket.pool.ts index d6f9959..7c7d6c3 100644 --- a/src/relay/udp.socket.pool.ts +++ b/src/relay/udp.socket.pool.ts @@ -32,7 +32,7 @@ export class UDPSocketPool { * @returns Allocated port * @throws if allocation fails */ - allocatePort(port: number = 0): Promise { + allocatePort(port = 0): Promise { return new Promise((resolve, reject) => { const socket = dgram.createSocket("udp4"); socket.once("error", reject); @@ -84,7 +84,7 @@ export class UDPSocketPool { */ getPort(): number { assert(this.freePorts.length > 0, "No more free ports!"); - return this.freePorts.pop()!!; + return this.freePorts.pop()!; } /** diff --git a/src/utils.ts b/src/utils.ts index eb68a4d..99f2b93 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,7 @@ export function sleep(seconds: number, value?: T): Promise { /** * Wait for an event on event source. */ -export function promiseEvent( +export function promiseEvent( source: EventEmitter, event: string, ): Promise { @@ -55,13 +55,14 @@ export function asSingletonFactory(f: () => T): () => T { * NOTE: The cache is not limited in any way, use only in cases where * the possible number of parameters is limited. */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function memoize(f: Function): Function { const cache = new Map(); - return function() { - const key = JSON.stringify(arguments); + return function(...args: unknown[]) { + const key = JSON.stringify(args); if (!cache.has(key)) { - cache.set(key, f(...arguments)); + cache.set(key, f(...args)); } return cache.get(key); @@ -128,11 +129,11 @@ export function range(n: number): number[] { * @returns {Array>} Array of combinations * @template T */ -export function combine(...arrays: Array): Array> { +export function combine(...arrays: unknown[][]): unknown[][] { const count = arrays.map((a) => a.length).reduce((a, b) => a * b, 1); const dimensions = arrays.length; - const result = range(count) as any[]; + const result = range(count).map(() => new Array(dimensions)); for (let i = 0; i < count; ++i) { const item = new Array(dimensions); @@ -210,7 +211,8 @@ export function formatDuration(seconds: number): string { }).reverse(); const [unit, multiplier] = - units.find(([_, f]) => seconds > f) ?? units.at(-1)!!; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + units.find(([_, f]) => seconds > f) ?? units.at(-1)!; return seconds / multiplier + unit; } @@ -220,7 +222,7 @@ export function formatDuration(seconds: number): string { * * Used to generate a word based OID For example, FalconTimberYolk */ -export function generateWordId(wordCount: number = 3): string { +export function generateWordId(wordCount = 3): string { return range(wordCount) .map(() => words[randomInt(words.length)]) .join(""); diff --git a/src/wordlist.ts b/src/wordlist.ts index cf7f104..588c904 100644 --- a/src/wordlist.ts +++ b/src/wordlist.ts @@ -1,2640 +1,2640 @@ export default [ - "Abacus", - "Accordion", - "Acorn", - "Acrobat", - "Adult", - "Aeroplane", - "Air", - "Airplane", - "Alarm", - "Albatross", - "Algae", - "Almond", - "Alphabet", - "Ambulance", - "America", - "Anatomy", - "Anchor", - "Andromeda", - "Angel", - "Ant", - "Apartment", - "Apology", - "Apparatus", - "Apparel", - "Appeal", - "Appendix", - "Apple", - "Apricot", - "Aquarium", - "Arc", - "Archipelago", - "Architecture", - "Argument", - "Armadillo", - "Armchair", - "Armor", - "Army", - "Arrow", - "Artichoke", - "Artist", - "Astronaut", - "Asylum", - "Athlete", - "Atmosphere", - "Atom", - "Audience", - "Aurora", - "Author", - "Automobile", - "Autumn", - "Avalanche", - "Avenue", - "Avocado", - "Award", - "Awkwardness", - "Axe", - "Baboon", - "Baby", - "Backbone", - "Backpack", - "Bacon", - "Badger", - "Bag", - "Baggage", - "Bagpipe", - "Bail", - "Bait", - "Baker", - "Balance", - "Balcony", - "Ball", - "Balloon", - "Ballot", - "Bamboo", - "Banana", - "Bandana", - "Bandit", - "Bangle", - "Banyan", - "Bar", - "Barbecue", - "Barber", - "Bargain", - "Bark", - "Barley", - "Barn", - "Barrel", - "Basement", - "Basket", - "Basketball", - "Bassoon", - "Bat", - "Bath", - "Battery", - "Battle", - "Bay", - "Beach", - "Bead", - "Beak", - "Beam", - "Bean", - "Bear", - "Beard", - "Beast", - "Beat", - "Beauty", - "Beaver", - "Bed", - "Bedroom", - "Bee", - "Beech", - "Beef", - "Beer", - "Beetle", - "Beggar", - "Beginner", - "Beginning", - "Behavior", - "Bell", - "Bellows", - "Belt", - "Bench", - "Bicycle", - "Bigfoot", - "Bike", - "Bill", - "Bird", - "Birth", - "Biscuit", - "Bit", - "Blackbird", - "Blackboard", - "Blackcurrant", - "Bladder", - "Blade", - "Blanket", - "Blazer", - "Blizzard", - "Block", - "Blood", - "Blouse", - "Blueberry", - "Boar", - "Board", - "Boat", - "Body", - "Bologna", - "Bolt", - "Bomb", - "Bone", - "Bonfire", - "Bonnet", - "Book", - "Bookcase", - "Boomerang", - "Boot", - "Border", - "Bottle", - "Bottom", - "Boulder", - "Bouquet", - "Bow", - "Bowl", - "Bowler", - "Box", - "Boy", - "Brain", - "Brake", - "Branch", - "Brass", - "Bread", - "Breast", - "Breath", - "Breeze", - "Brick", - "Bridge", - "Briefcase", - "Brilliance", - "Broccoli", - "Bronze", - "Brother", - "Brownie", - "Brush", - "Bubble", - "Bucket", - "Buffalo", - "Bug", - "Building", - "Bulb", - "Bull", - "Bumblebee", - "Bunker", - "Bureau", - "Burger", - "Burglary", - "Bus", - "Bush", - "Business", - "Bust", - "Butter", - "Butterfly", - "Button", - "Buzzard", - "Cabbage", - "Cabin", - "Cabinet", - "Cable", - "Cactus", - "Cadet", - "Cafe", - "Cage", - "Cake", - "Calculator", - "Calf", - "Calm", - "Camel", - "Camera", - "Camp", - "Can", - "Canal", - "Candle", - "Candy", - "Cane", - "Cannibal", - "Cannon", - "Canoe", - "Canyon", - "Cap", - "Cape", - "Capital", - "Capitalism", - "Capitol", - "Captain", - "Car", - "Caravan", - "Carbon", - "Card", - "Cardboard", - "Care", - "Career", - "Cargo", - "Carpet", - "Carrot", - "Cart", - "Carton", - "Cartoon", - "Case", - "Cash", - "Castle", - "Cat", - "Caterpillar", - "Cathedral", - "Cattle", - "Cave", - "Cavern", - "CD", - "Cedar", - "Cell", - "Cellar", - "Cello", - "Cellophane", - "Cement", - "Cemetery", - "Cereal", - "Chain", - "Chair", - "Chalk", - "Challenge", - "Chamber", - "Champagne", - "Champion", - "Chance", - "Channel", - "Chaos", - "Chapel", - "Chapter", - "Character", - "Charity", - "Charm", - "Chart", - "Chase", - "Chateau", - "Cheese", - "Cheetah", - "Chef", - "Chemistry", - "Cherry", - "Chess", - "Chest", - "Chicken", - "Child", - "Chimpanzee", - "Chin", - "Chip", - "Chocolate", - "Choice", - "Choir", - "Chowder", - "Christmas", - "Chrome", - "Church", - "Cider", - "Cigar", - "Cigarette", - "Cinema", - "Circle", - "Circus", - "City", - "Clam", - "Clarity", - "Class", - "Classic", - "Classroom", - "Claw", - "Clay", - "Cleaner", - "Clearing", - "Cleavage", - "Clerk", - "Cliff", - "Climate", - "Climax", - "Climbing", - "Clinic", - "Clip", - "Clock", - "Closet", - "Cloth", - "Clothes", - "Cloud", - "Clover", - "Club", - "Coach", - "Coal", - "Coast", - "Coat", - "Cobra", - "Cobweb", - "Cocktail", - "Cocoa", - "Coffee", - "Coffin", - "Coin", - "Cola", - "Cold", - "Collar", - "Collection", - "College", - "Colony", - "Color", - "Column", - "Comb", - "Comet", - "Comfort", - "Comic", - "Command", - "Comment", - "Commerce", - "Committee", - "Common", - "Communication", - "Community", - "Company", - "Compass", - "Competition", - "Composer", - "Compromise", - "Computer", - "Concentration", - "Concept", - "Concern", - "Concert", - "Conclusion", - "Concrete", - "Condition", - "Conduct", - "Cone", - "Conference", - "Confession", - "Confidence", - "Conflict", - "Confusion", - "Congo", - "Congress", - "Conifer", - "Connection", - "Consequence", - "Conservatory", - "Consideration", - "Consolation", - "Conspiracy", - "Constant", - "Construction", - "Consul", - "Container", - "Content", - "Contention", - "Continent", - "Contract", - "Control", - "Cook", - "Cooperation", - "Copper", - "Copy", - "Coral", - "Cord", - "Cork", - "Corn", - "Corner", - "Coronation", - "Corporation", - "Corridor", - "Corruption", - "Cosmos", - "Cost", - "Costume", - "Cottage", - "Cotton", - "Cough", - "Council", - "Count", - "Counter", - "Country", - "Courage", - "Course", - "Court", - "Cousin", - "Covenant", - "Cover", - "Cow", - "Cowboy", - "Crab", - "Cracker", - "Cradle", - "Craft", - "Crane", - "Crate", - "Crater", - "Crayon", - "Cream", - "Creativity", - "Creature", - "Creek", - "Crepe", - "Crib", - "Cricket", - "Crime", - "Crisis", - "Criterion", - "Criticism", - "Crocodile", - "Crop", - "Cross", - "Crow", - "Crown", - "Cruiser", - "Crumb", - "Crush", - "Cry", - "Crystal", - "Cube", - "Cuckoo", - "Cucumber", - "Cult", - "Culture", - "Cup", - "Cupboard", - "Curiosity", - "Curtain", - "Cushion", - "Custom", - "Cut", - "Cutter", - "Cycle", - "Cylinder", - "Cymbal", - "Cypress", - "Daddy", - "Daffodil", - "Dagger", - "Dahlia", - "Dairy", - "Dam", - "Damage", - "Dance", - "Danger", - "Dare", - "Darkness", - "Dart", - "Dash", - "Database", - "Date", - "Daughter", - "Dawn", - "Day", - "Daylight", - "Deacon", - "Deal", - "Dean", - "Death", - "Debate", - "Debt", - "Decade", - "Decay", - "Deception", - "Decision", - "Deck", - "Decline", - "Decoration", - "Decree", - "Deer", - "Defence", - "Deficit", - "Definition", - "Degree", - "Delay", - "Delegation", - "Delight", - "Delivery", - "Demand", - "Democracy", - "Demon", - "Den", - "Dentist", - "Department", - "Departure", - "Deposit", - "Depression", - "Depth", - "Deputy", - "Desert", - "Desire", - "Desk", - "Despair", - "Dessert", - "Destination", - "Detail", - "Detective", - "Determinant", - "Development", - "Device", - "Devotion", - "Dew", - "Diagram", - "Dialect", - "Diamond", - "Diaphragm", - "Diary", - "Dictator", - "Dictionary", - "Dinosaur", - "Diploma", - "Direction", - "Director", - "Dirt", - "Disability", - "Disadvantage", - "Disaster", - "Disc", - "Discipline", - "Disclosure", - "Discount", - "Discovery", - "Discussion", - "Disease", - "Dish", - "Disk", - "Disposal", - "Distance", - "Distinction", - "Disturbance", - "Dive", - "Diversity", - "Division", - "Doctor", - "Doctrine", - "Document", - "Dog", - "Dollar", - "Doll", - "Dolphin", - "Donation", - "Donkey", - "Door", - "Doorknob", - "Dot", - "Doubt", - "Dough", - "Dove", - "Downpour", - "Dragon", - "Drain", - "Drama", - "Drawer", - "Dream", - "Dressing", - "Drill", - "Drink", - "Drive", - "Driver", - "Driving", - "Drizzle", - "Drone", - "Drop", - "Drum", - "Duck", - "Dune", - "Dungeon", - "Durian", - "Dust", - "Duty", - "Dwarf", - "Dye", - "Eagle", - "Ear", - "Earth", - "Earthquake", - "Ease", - "East", - "Echo", - "Economy", - "Edge", - "Edifice", - "Editor", - "Education", - "Effect", - "Effort", - "Egg", - "Eggplant", - "Ego", - "Elbow", - "Election", - "Electricity", - "Elephant", - "Elevator", - "Elite", - "Elk", - "Embarrassment", - "Embrace", - "Emerald", - "Emotion", - "Emperor", - "Emphasis", - "Empire", - "Employee", - "Employer", - "Employment", - "End", - "Endurance", - "Energy", - "Engine", - "Engineer", - "Enigma", - "Enjoyment", - "Enough", - "Enquiry", - "Enterprise", - "Entertainment", - "Enthusiasm", - "Entrance", - "Environment", - "Episode", - "Equality", - "Equation", - "Equinox", - "Equipment", - "Era", - "Eruption", - "Escalator", - "Escape", - "Espresso", - "Essence", - "Estate", - "Estimate", - "Eternity", - "Ethics", - "Europe", - "Evaluation", - "Eve", - "Evening", - "Event", - "Evidence", - "Evil", - "Evolution", - "Example", - "Excavation", - "Excellence", - "Exception", - "Exchange", - "Excitement", - "Excuse", - "Execution", - "Executive", - "Exercise", - "Exhibition", - "Existence", - "Exit", - "Expansion", - "Expectation", - "Expedition", - "Expense", - "Experience", - "Experiment", - "Expert", - "Explanation", - "Exploration", - "Explosion", - "Exposure", - "Extension", - "Extent", - "External", - "Eye", - "Eyeball", - "Eyebrow", - "Eyelash", - "Eyelid", - "Fabric", - "Face", - "Facility", - "Fact", - "Factor", - "Faculty", - "Fading", - "Failure", - "Fair", - "Faith", - "Falcon", - "Fall", - "Family", - "Fan", - "Fantasy", - "Farm", - "Farmer", - "Fascination", - "Fashion", - "Fate", - "Father", - "Fatigue", - "Fault", - "Fear", - "Feast", - "Feather", - "Feature", - "Federation", - "Fee", - "Feedback", - "Feeling", - "Feet", - "Fence", - "Ferret", - "Festival", - "Fiber", - "Fiction", - "Fiddle", - "Field", - "Fig", - "Fight", - "Figure", - "File", - "Film", - "Filter", - "Fin", - "Finance", - "Finding", - "Finger", - "Finish", - "Fire", - "Firefighter", - "Firefly", - "Firm", - "Fish", - "Fisherman", - "Fitness", - "Fix", - "Fixture", - "Flag", - "Flame", - "Flamingo", - "Flash", - "Flax", - "Flea", - "Fleet", - "Flesh", - "Flight", - "Flock", - "Flood", - "Floor", - "Flour", - "Flower", - "Fluid", - "Flute", - "Fly", - "Foam", - "Fog", - "Foil", - "Folder", - "Folk", - "Food", - "Foot", - "Football", - "Force", - "Forecast", - "Forehead", - "Forest", - "Fork", - "Form", - "Format", - "Former", - "Fort", - "Fortune", - "Foundation", - "Fountain", - "Fox", - "Fraction", - "Fragment", - "Frame", - "France", - "Freedom", - "Freeze", - "Freezer", - "Freight", - "French", - "Frequency", - "Friction", - "Friend", - "Frog", - "Front", - "Frost", - "Frown", - "Fruit", - "Fuel", - "Function", - "Funding", - "Funeral", - "Fur", - "Furnace", - "Furniture", - "Future", - "Gadget", - "Gaffer", - "Gain", - "Gait", - "Galaxy", - "Gallery", - "Galleon", - "Gallon", - "Game", - "Garden", - "Garlic", - "Garment", - "Gas", - "Gate", - "Gathering", - "Gauge", - "Gazelle", - "Gear", - "Geese", - "Gem", - "Gender", - "Gene", - "General", - "Genius", - "Genre", - "Gentleman", - "Geography", - "Geology", - "Geometry", - "Geranium", - "Germany", - "Gesture", - "Ghost", - "Giant", - "Gift", - "Giraffe", - "Girl", - "Glacier", - "Gladness", - "Glass", - "Glaze", - "Glimpse", - "Globe", - "Gloom", - "Glory", - "Glove", - "Glow", - "Glue", - "Goal", - "Goat", - "God", - "Gold", - "Goldfish", - "Golf", - "Gondola", - "Gong", - "Good", - "Goodness", - "Goose", - "Gorilla", - "Gossip", - "Government", - "Governor", - "Gown", - "Grace", - "Grade", - "Grain", - "Grammar", - "Grandchild", - "Grandfather", - "Grandmother", - "Grandparent", - "Grandson", - "Grape", - "Graph", - "Grass", - "Grasshopper", - "Gratitude", - "Grave", - "Gravity", - "Grease", - "Greatness", - "Greenhouse", - "Greeting", - "Grid", - "Grief", - "Grille", - "Grimace", - "Grind", - "Grit", - "Grocery", - "Ground", - "Group", - "Grouse", - "Growth", - "Guarantee", - "Guard", - "Guess", - "Guest", - "Guidance", - "Guide", - "Guitar", - "Gum", - "Gun", - "Gutter", - "Gym", - "Habitat", - "Hair", - "Haircut", - "Hall", - "Hamburger", - "Hammer", - "Hamster", - "Hand", - "Handbag", - "Handicap", - "Handle", - "Handsaw", - "Happiness", - "Harbor", - "Hardship", - "Hare", - "Harm", - "Harmony", - "Harp", - "Harvest", - "Hat", - "Hate", - "Hawk", - "Hazard", - "Head", - "Headache", - "Headland", - "Headlight", - "Headline", - "Headquarters", - "Health", - "Hearing", - "Heart", - "Heat", - "Heather", - "Heaven", - "Hedge", - "Hedgehog", - "Height", - "Helicopter", - "Hell", - "Helmet", - "Help", - "Hemisphere", - "Herb", - "Heron", - "Hero", - "Herring", - "Hesitation", - "Hide", - "Highway", - "Hill", - "Hip", - "Hippopotamus", - "Hire", - "Historian", - "History", - "Hobbit", - "Hockey", - "Hold", - "Hole", - "Holiday", - "Holland", - "Home", - "Honey", - "Honeydew", - "Honesty", - "Honor", - "Hood", - "Hoof", - "Hook", - "Hope", - "Horizon", - "Horn", - "Horse", - "Hospital", - "Host", - "Hostel", - "Hotdog", - "Hotel", - "Hour", - "House", - "Hovercraft", - "Hummingbird", - "Humor", - "Hunter", - "Hurricane", - "Hurry", - "Husband", - "Hut", - "Hyacinth", - "Hydrant", - "Hyena", - "Hygiene", - "Hypothesis", - "Ice", - "Iceberg", - "Icicle", - "Idea", - "Ideal", - "Identity", - "Ideology", - "Idiot", - "Igloo", - "Ignorance", - "Illusion", - "Image", - "Imagination", - "Impact", - "Implement", - "Importance", - "Impression", - "Improvement", - "Incense", - "Incentive", - "Inch", - "Incidence", - "Income", - "Increase", - "Independence", - "Index", - "India", - "Indication", - "Individual", - "Industry", - "Infant", - "Infection", - "Influence", - "Information", - "Ingenuity", - "Ingredient", - "Inhabitant", - "Inheritance", - "Initial", - "Initiative", - "Injection", - "Injury", - "Ink", - "Inquiry", - "Insect", - "Inside", - "Insight", - "Insistence", - "Inspection", - "Inspector", - "Inspiration", - "Installment", - "Instance", - "Instinct", - "Institute", - "Institution", - "Instruction", - "Instrument", - "Insulation", - "Insurance", - "Intelligence", - "Intention", - "Interest", - "Interference", - "Interior", - "Internet", - "Interpretation", - "Interruption", - "Interview", - "Introduction", - "Intuition", - "Invention", - "Inventor", - "Investment", - "Invitation", - "Invoice", - "Iron", - "Island", - "Issue", - "Italy", - "Item", - "Jackal", - "Jacket", - "Jaguar", - "Jail", - "Jalapeño", - "Jam", - "Jar", - "Jasmine", - "Javelin", - "Jaw", - "Jeans", - "Jelly", - "Jellyfish", - "Jet", - "Jewel", - "Jewelry", - "Job", - "Jockey", - "Joker", - "Journey", - "Joy", - "Judge", - "Judgment", - "Juice", - "Jumper", - "Junction", - "Jungle", - "Juniper", - "Junk", - "Justice", - "Kangaroo", - "Karate", - "Kayaking", - "Ketchup", - "Kettle", - "Key", - "Keyboard", - "Kick", - "Kid", - "Kidney", - "Kilometer", - "King", - "Kingdom", - "Kiss", - "Kitchen", - "Kite", - "Kitten", - "Knapsack", - "Knee", - "Kneel", - "Knife", - "Knight", - "Knitting", - "Knob", - "Knock", - "Knot", - "Knowledge", - "Koala", - "Lab", - "Label", - "Labor", - "Laboratory", - "Lace", - "Lack", - "Ladder", - "Lad", - "Lady", - "Lake", - "Lamb", - "Lamp", - "Lancet", - "Land", - "Landscape", - "Lane", - "Language", - "Lantern", - "Lap", - "Lapel", - "Larch", - "Lard", - "Lark", - "Laughter", - "Launch", - "Lavender", - "Law", - "Lawn", - "Lawyer", - "Lead", - "Leader", - "Leaf", - "League", - "Leak", - "Leather", - "Lecture", - "Leg", - "Legacy", - "Legal", - "Legend", - "Legislation", - "Leisure", - "Lemon", - "Length", - "Lentil", - "Leopard", - "Letter", - "Lettuce", - "Level", - "Lever", - "Liability", - "Library", - "License", - "Lid", - "Life", - "Lift", - "Light", - "Lightning", - "Lilac", - "Lily", - "Limb", - "Lime", - "Limit", - "Line", - "Linen", - "Link", - "Lion", - "Lip", - "Lipstick", - "Liquid", - "List", - "Listen", - "Literacy", - "Literature", - "Litter", - "Liver", - "Lizard", - "Llama", - "Loaf", - "Lobby", - "Lobster", - "Local", - "Location", - "Lock", - "Locker", - "Locust", - "Loft", - "Log", - "Logic", - "Lollipop", - "Loneliness", - "Longboat", - "Look", - "Loop", - "Lord", - "Loss", - "Lot", - "Lotion", - "Lounge", - "Love", - "Lover", - "Loyalty", - "Luck", - "Luggage", - "Lullaby", - "Lunch", - "Lungs", - "Lute", - "Luxury", - "Lynx", - "Lyre", - "Macaroon", - "Machine", - "Madness", - "Magazine", - "Magic", - "Magnet", - "Magnitude", - "Maiden", - "Mail", - "Mailbox", - "Mainland", - "Maintenance", - "Majority", - "Make", - "Maker", - "Malaria", - "Male", - "Mall", - "Man", - "Management", - "Manager", - "Mango", - "Mansion", - "Manual", - "Manufacture", - "Map", - "Maple", - "Marble", - "March", - "Mare", - "Margin", - "Mark", - "Market", - "Marketing", - "Marmalade", - "Marriage", - "Marsh", - "Marshmallow", - "Mask", - "Mass", - "Master", - "Match", - "Material", - "Math", - "Matter", - "Mattress", - "Maximum", - "Mayonnaise", - "Maze", - "Meadow", - "Meal", - "Meaning", - "Measure", - "Meat", - "Mechanic", - "Mechanism", - "Medal", - "Medicine", - "Mediocrity", - "Meeting", - "Melody", - "Melon", - "Member", - "Memo", - "Memory", - "Men", - "Mention", - "Menu", - "Mercy", - "Mess", - "Message", - "Metal", - "Meter", - "Method", - "Mexico", - "Microphone", - "Microscope", - "Midday", - "Middle", - "Midnight", - "Might", - "Migration", - "Milk", - "Mill", - "Millennium", - "Mind", - "Mine", - "Mineral", - "Minibus", - "Minimum", - "Minister", - "Minority", - "Mint", - "Minute", - "Mirror", - "Missile", - "Mission", - "Mist", - "Mistake", - "Mister", - "Mitten", - "Mix", - "Mixture", - "Mobile", - "Mode", - "Model", - "Modesty", - "Mole", - "Moment", - "Monarch", - "Money", - "Monitor", - "Monkey", - "Monocle", - "Monster", - "Month", - "Monument", - "Mood", - "Moon", - "Moose", - "Moral", - "Morning", - "Moth", - "Mother", - "Motion", - "Motivation", - "Motor", - "Motorcycle", - "Mound", - "Mountain", - "Mouse", - "Mouth", - "Movement", - "Movie", - "Muffin", - "Mule", - "Multiple", - "Mummy", - "Muscle", - "Museum", - "Mushroom", - "Music", - "Musician", - "Mustard", - "Mystery", - "Myth", - "Nail", - "Name", - "Napkin", - "Nation", - "Nature", - "Navel", - "Navigation", - "Nebula", - "Neck", - "Necklace", - "Need", - "Needle", - "Negative", - "Negotiation", - "Neighbor", - "Neighborhood", - "Nerve", - "Nest", - "Net", - "Network", - "News", - "Newspaper", - "Niche", - "Night", - "Nightmare", - "Nightingale", - "Noise", - "Noodle", - "North", - "Nose", - "Note", - "Notebook", - "Nothing", - "Notice", - "Notion", - "Novel", - "Novelty", - "Nuisance", - "Number", - "Nurse", - "Nut", - "Nutrition", - "Nylon", - "Oak", - "Oar", - "Oasis", - "Objection", - "Objective", - "Obligation", - "Observation", - "Observer", - "Obstacle", - "Occasion", - "Occupation", - "Ocean", - "Ocelot", - "Octopus", - "Offence", - "Offer", - "Office", - "Officer", - "Official", - "Oil", - "Ointment", - "Old", - "Olive", - "Omnibus", - "Onion", - "Open", - "Opening", - "Opera", - "Operation", - "Opinion", - "Opportunity", - "Opponent", - "Optics", - "Option", - "Orange", - "Orchestra", - "Order", - "Organ", - "Organization", - "Orientation", - "Origin", - "Ornament", - "Ostrich", - "Otter", - "Outcome", - "Outfit", - "Outlet", - "Outline", - "Output", - "Oval", - "Oven", - "Overalls", - "Overcoat", - "Overhang", - "Overload", - "Owl", - "Owner", - "Oxygen", - "Oyster", - "Pace", - "Package", - "Packet", - "Pad", - "Page", - "Pain", - "Paint", - "Painting", - "Pair", - "Palace", - "Paleontologist", - "Palm", - "Pan", - "Panda", - "Panel", - "Panic", - "Pants", - "Paper", - "Paperback", - "Parade", - "Paradise", - "Paragraph", - "Parallel", - "Parasite", - "Parent", - "Park", - "Parrot", - "Part", - "Partner", - "Party", - "Passage", - "Past", - "Pastor", - "Patch", - "Path", - "Patience", - "Patient", - "Patio", - "Patrol", - "Pattern", - "Pause", - "Pavement", - "Paw", - "Payment", - "Pea", - "Peace", - "Peach", - "Peak", - "Pear", - "Pearl", - "Peasant", - "Pebble", - "Pedal", - "Pedestrian", - "Peel", - "Pen", - "Penalty", - "Pencil", - "Penguin", - "Pension", - "People", - "Pepper", - "Percentage", - "Perception", - "Performance", - "Period", - "Permission", - "Permit", - "Persistence", - "Person", - "Personality", - "Perspective", - "Pet", - "Petition", - "Phase", - "Pheasant", - "Phenomenon", - "Philosophy", - "Phone", - "Photo", - "Photography", - "Phrase", - "Physics", - "Piano", - "Pickle", - "Picture", - "Pie", - "Piece", - "Pig", - "Pigeon", - "Pigment", - "Pile", - "Pill", - "Pillar", - "Pillow", - "Pilot", - "Pine", - "Pineapple", - "Pink", - "Pint", - "Pipe", - "Pistol", - "Pit", - "Pitch", - "Pizza", - "Place", - "Plain", - "Plan", - "Plane", - "Planet", - "Plant", - "Plastic", - "Plate", - "Platform", - "Play", - "Player", - "Playground", - "Pleasure", - "Pledge", - "Plot", - "Plug", - "Plum", - "Plumber", - "Pocket", - "Poem", - "Poet", - "Point", - "Poison", - "Pole", - "Police", - "Policy", - "Politeness", - "Poll", - "Pollen", - "Polo", - "Pond", - "Pony", - "Pool", - "Popcorn", - "Pope", - "Poplar", - "Poppy", - "Popularity", - "Population", - "Porcelain", - "Porch", - "Porcupine", - "Port", - "Porter", - "Portfolio", - "Portion", - "Portrait", - "Position", - "Possession", - "Possibility", - "Post", - "Postage", - "Poster", - "Pot", - "Potato", - "Potential", - "Pound", - "Poverty", - "Powder", - "Power", - "Practice", - "Praise", - "Pray", - "Prayer", - "Preacher", - "Precedence", - "Precision", - "Prediction", - "Preference", - "Preparation", - "Presence", - "Present", - "Presentation", - "Preservation", - "President", - "Press", - "Pressure", - "Prestige", - "Prevention", - "Price", - "Pride", - "Priest", - "Primary", - "Prime", - "Prince", - "Princess", - "Principal", - "Principle", - "Print", - "Prior", - "Priority", - "Prison", - "Prisoner", - "Privacy", - "Prize", - "Problem", - "Procedure", - "Process", - "Prodigy", - "Produce", - "Product", - "Profession", - "Professor", - "Profit", - "Program", - "Progress", - "Project", - "Promise", - "Promotion", - "Proof", - "Property", - "Proposal", - "Prosecution", - "Prospect", - "Protection", - "Protest", - "Proud", - "Province", - "Provision", - "Psychology", - "Pub", - "Public", - "Publication", - "Publicity", - "Publisher", - "Pudding", - "Puddle", - "Puffin", - "Pull", - "Pulpit", - "Pulse", - "Puma", - "Pump", - "Pumpkin", - "Punch", - "Punctuation", - "Punishment", - "Puppet", - "Puppy", - "Purchase", - "Purpose", - "Purse", - "Pursuit", - "Push", - "Put", - "Pyramid", - "Quadrant", - "Quail", - "Quality", - "Quantity", - "Quartz", - "Queen", - "Quill", - "Quilt", - "Quince", - "Quit", - "Rabbit", - "Raccoon", - "Race", - "Rack", - "Radar", - "Radiator", - "Radio", - "Radish", - "Raft", - "Rag", - "Rail", - "Rain", - "Rainbow", - "Raincoat", - "Ram", - "Range", - "Rank", - "Rapids", - "Rate", - "Raven", - "Raw", - "Ray", - "Razor", - "Reaction", - "Reader", - "Reading", - "Reality", - "Reason", - "Reception", - "Recipe", - "Recognition", - "Recommendation", - "Record", - "Recording", - "Recovery", - "Recreation", - "Rectangle", - "Redcurrant", - "Redoubt", - "Reed", - "Reference", - "Refinement", - "Reflection", - "Reform", - "Refusal", - "Regard", - "Regret", - "Regular", - "Regulation", - "Rein", - "Reindeer", - "Relation", - "Relationship", - "Relaxation", - "Release", - "Relief", - "Religion", - "Remark", - "Remedy", - "Reminder", - "Removal", - "Rent", - "Repair", - "Repeat", - "Replacement", - "Reply", - "Report", - "Reporter", - "Representation", - "Representative", - "Reproduction", - "Reputation", - "Request", - "Requirement", - "Research", - "Reserve", - "Resident", - "Resistance", - "Resolution", - "Resort", - "Resource", - "Respect", - "Response", - "Responsibility", - "Rest", - "Restaurant", - "Result", - "Retail", - "Retirement", - "Return", - "Revelation", - "Revenge", - "Revenue", - "Reverse", - "Review", - "Revolution", - "Reward", - "Rhetoric", - "Rhinoceros", - "Rhythm", - "Rib", - "Ribbon", - "Rice", - "Riddle", - "Ridge", - "Rifle", - "Right", - "Ring", - "Rip", - "Rise", - "Risk", - "Ritual", - "River", - "Road", - "Roadrunner", - "Roast", - "Robbery", - "Robot", - "Rock", - "Rocket", - "Rodent", - "Role", - "Roll", - "Romance", - "Roof", - "Room", - "Rooster", - "Root", - "Rope", - "Rose", - "Rotation", - "Rough", - "Round", - "Route", - "Routine", - "Row", - "Royal", - "Rubbish", - "Ruby", - "Rucksack", - "Ruin", - "Rule", - "Ruler", - "Rumor", - "Run", - "Runner", - "Rush", - "Rust", - "Rye", - "Sack", - "Sacrifice", - "Sadness", - "Saddle", - "Safari", - "Safety", - "Saffron", - "Sage", - "Sail", - "Sailor", - "Salad", - "Salary", - "Sale", - "Salmon", - "Salt", - "Sample", - "Sand", - "Sandal", - "Sandwich", - "Sapphire", - "Satellite", - "Satisfaction", - "Sauce", - "Saucepan", - "Sausage", - "Save", - "Saw", - "Scale", - "Scandal", - "Scarecrow", - "Scarf", - "Scene", - "Schedule", - "Scheme", - "Scholar", - "Scholarship", - "School", - "Science", - "Scientist", - "Scoop", - "Scope", - "Score", - "Scorpion", - "Scout", - "Scrabble", - "Scramble", - "Scrap", - "Scratch", - "Screen", - "Screw", - "Script", - "Sculpture", - "Sea", - "Seagull", - "Seal", - "Search", - "Seashore", - "Season", - "Seat", - "Secret", - "Secretary", - "Section", - "Sector", - "Security", - "Seed", - "Seek", - "Selection", - "Self", - "Seller", - "Sense", - "Sensitivity", - "Sentence", - "Separation", - "September", - "Sequence", - "Series", - "Serve", - "Server", - "Service", - "Session", - "Set", - "Setting", - "Settle", - "Settlement", - "Shade", - "Shadow", - "Shake", - "Shame", - "Shampoo", - "Shape", - "Share", - "Shark", - "Sheep", - "Sheet", - "Shelf", - "Shell", - "Shelter", - "Shepherd", - "Shield", - "Shift", - "Shine", - "Ship", - "Shirt", - "Shock", - "Shoe", - "Shooting", - "Shop", - "Shopping", - "Shore", - "Short", - "Shortage", - "Shot", - "Shoulder", - "Shout", - "Show", - "Shower", - "Shrimp", - "Shrine", - "Sibling", - "Sick", - "Side", - "Sidewalk", - "Sight", - "Sign", - "Signal", - "Signature", - "Significance", - "Silence", - "Silk", - "Silver", - "Similarity", - "Simon", - "Simple", - "Simplicity", - "Sin", - "Singer", - "Single", - "Sink", - "Sister", - "Site", - "Situation", - "Size", - "Sketch", - "Ski", - "Skill", - "Skin", - "Skirt", - "Skull", - "Sky", - "Slab", - "Slang", - "Slave", - "Slavery", - "Sleep", - "Sleeve", - "Slice", - "Slide", - "Slip", - "Slope", - "Slot", - "Sly", - "Smell", - "Smile", - "Smoke", - "Snail", - "Snake", - "Snow", - "Soap", - "Society", - "Sock", - "Soda", - "Softness", - "Soil", - "Solar", - "Soldier", - "Sole", - "Solicitor", - "Solid", - "Solution", - "Somersault", - "Son", - "Song", - "Soprano", - "Sore", - "Sort", - "Soul", - "Sound", - "Soup", - "Source", - "South", - "Sovereignty", - "Space", - "Spade", - "Spaghetti", - "Spain", - "Span", - "Sparrow", - "Speaker", - "Special", - "Specialist", - "Species", - "Specification", - "Speech", - "Speed", - "Spell", - "Spice", - "Spider", - "Spike", - "Spinach", - "Spirit", - "Spite", - "Splash", - "Spleen", - "Splendor", - "Split", - "Spoon", - "Sport", - "Spot", - "Spring", - "Squad", - "Square", - "Squash", - "Squirrel", - "Stability", - "Stadium", - "Staff", - "Stage", - "Stain", - "Stair", - "Stake", - "Stall", - "Stamp", - "Stance", - "Stand", - "Standard", - "Star", - "Stare", - "Start", - "State", - "Statement", - "Station", - "Statistic", - "Status", - "Steak", - "Steam", - "Steel", - "Steering", - "Step", - "Stick", - "Sticker", - "Stomach", - "Stone", - "Stool", - "Stop", - "Storage", - "Store", - "Storm", - "Story", - "Stove", - "Strain", - "Strait", - "Strand", - "Stranger", - "Strategy", - "Strawberry", - "Stream", - "Street", - "Strength", - "Stress", - "Stretch", - "Strike", - "String", - "Strip", - "Structure", - "Struggle", - "Student", - "Studio", - "Study", - "Stuff", - "Stupidity", - "Style", - "Subject", - "Submission", - "Substance", - "Success", - "Sugar", - "Suggestion", - "Suit", - "Sulphur", - "Summit", - "Sun", - "Sunshine", - "Supermarket", - "Supply", - "Support", - "Surgeon", - "Surgery", - "Surname", - "Surprise", - "Surrender", - "Surroundings", - "Survey", - "Survival", - "Suspicion", - "Swallow", - "Swan", - "Swear", - "Sweater", - "Sweep", - "Sweet", - "Sweetness", - "Swim", - "Swimming", - "Swine", - "Swing", - "Switch", - "Sword", - "Symbol", - "Sympathy", - "Syrup", - "System", - "Table", - "Tablet", - "Taboo", - "Tackle", - "Tail", - "Tailor", - "Talent", - "Talk", - "Tank", - "Tap", - "Tape", - "Target", - "Task", - "Taste", - "Tattoo", - "Tax", - "Taxi", - "Tea", - "Teacher", - "Team", - "Tear", - "Technique", - "Technology", - "Teeth", - "Telegram", - "Telephone", - "Telescope", - "Television", - "Temper", - "Temperature", - "Temple", - "Tempo", - "Temptation", - "Tendency", - "Tennis", - "Tension", - "Tent", - "Term", - "Territory", - "Terror", - "Test", - "Text", - "Texture", - "Thank", - "Thanks", - "Theme", - "Theory", - "Therapy", - "Thick", - "Thief", - "Thing", - "Think", - "Thinness", - "Thought", - "Thread", - "Threat", - "Throat", - "Throne", - "Thumb", - "Thunder", - "Ticket", - "Tide", - "Tiger", - "Tile", - "Time", - "Tin", - "Tip", - "Tire", - "Tissue", - "Title", - "Toad", - "Toast", - "Tobacco", - "Today", - "Toe", - "Together", - "Toilet", - "Toleration", - "Tomato", - "Tomorrow", - "Tongue", - "Tonight", - "Tool", - "Tooth", - "Toothbrush", - "Toothpaste", - "Top", - "Topic", - "Torch", - "Tornado", - "Tortoise", - "Torture", - "Total", - "Touch", - "Tough", - "Tour", - "Tourism", - "Tourist", - "Towel", - "Tower", - "Town", - "Toxic", - "Toy", - "Trace", - "Track", - "Tractor", - "Trade", - "Tradition", - "Traffic", - "Tragedy", - "Trail", - "Trailer", - "Train", - "Trainer", - "Training", - "Trait", - "Transaction", - "Transcript", - "Transfer", - "Transformation", - "Transition", - "Translation", - "Transmission", - "Transport", - "Trap", - "Travel", - "Tray", - "Treasure", - "Treat", - "Treatment", - "Tree", - "Trial", - "Triangle", - "Tribe", - "Trick", - "Trolley", - "Trouble", - "Trousers", - "Truck", - "Trumpet", - "Trunk", - "Trust", - "Truth", - "Tuba", - "Tube", - "Tulip", - "Tuna", - "Tunnel", - "Turkey", - "Turn", - "Turnip", - "Turtle", - "Twin", - "Twist", - "Type", - "Typhoon", - "Tyre", - "Uganda", - "Ukulele", - "Ultimate", - "Umbrella", - "Uncle", - "Under", - "Underpants", - "Understanding", - "Underwear", - "Unemployment", - "Uniform", - "Union", - "Unit", - "United", - "Unity", - "Universe", - "University", - "Unknown", - "Unrest", - "Uranus", - "Urgent", - "Urine", - "Usage", - "Use", - "User", - "Utility", - "Utopia", - "Vacancy", - "Vacation", - "Vacuum", - "Valley", - "Value", - "Van", - "Vanilla", - "Vanish", - "Variety", - "Vase", - "Vastness", - "Veal", - "Vector", - "Vegetable", - "Vehicle", - "Veil", - "Vein", - "Velvet", - "Vendor", - "Ventilation", - "Venture", - "Verdict", - "Verification", - "Verse", - "Version", - "Vessel", - "Veteran", - "Vet", - "Vibration", - "Vicinity", - "Victim", - "Victory", - "Video", - "View", - "Village", - "Vine", - "Vinegar", - "Violation", - "Violet", - "Violin", - "Viper", - "Virus", - "Visa", - "Vision", - "Visit", - "Visitor", - "Visual", - "Vitality", - "Vitamin", - "Vocation", - "Vodka", - "Voice", - "Volcano", - "Volleyball", - "Volume", - "Volunteer", - "Vomit", - "Vortex", - "Vote", - "Voucher", - "Vowel", - "Voyage", - "Vulture", - "Waddle", - "Wafer", - "Wage", - "Wagon", - "Waist", - "Waiter", - "Wake", - "Walk", - "Wall", - "Wallet", - "Walnut", - "Waltz", - "War", - "Ward", - "Warehouse", - "Warmth", - "Warning", - "Warrant", - "Warrior", - "Wash", - "Washer", - "Waste", - "Watch", - "Water", - "Waterfall", - "Wave", - "Wavelength", - "Wax", - "Way", - "Weakness", - "Wealth", - "Weapon", - "Weather", - "Web", - "Wedding", - "Wedge", - "Week", - "Weekend", - "Weight", - "Welcome", - "Welfare", - "Well", - "West", - "Whale", - "Wheat", - "Wheel", - "Whiskey", - "Whisper", - "Whistle", - "White", - "Whole", - "Wicket", - "Widow", - "Wife", - "Wild", - "Wilderness", - "Wildlife", - "Will", - "Willow", - "Win", - "Wind", - "Windmill", - "Window", - "Wine", - "Wing", - "Winner", - "Winter", - "Wire", - "Wisdom", - "Wish", - "Witness", - "Wolf", - "Woman", - "Wonder", - "Wood", - "Wool", - "Word", - "Work", - "Worker", - "World", - "Worm", - "Worry", - "Worship", - "Worth", - "Wound", - "Wrap", - "Wreck", - "Wrestler", - "Wrinkle", - "Wrist", - "Writer", - "Writing", - "Xenon", - "X-ray", - "Xylophone", - "Yacht", - "Yak", - "Yam", - "Yard", - "Yarn", - "Yawn", - "Year", - "Yeast", - "Yellow", - "Yen", - "Yoga", - "Yogurt", - "Yolk", - "Young", - "Youth", - "Yukon", - "Zebra", - "Zenith", - "Zephyr", - "Zero", - "Zigzag", - "Zinc", - "Zinnia", - "Zip", - "Zone", - "Zoo", - "Zoology", - "Zoom", -]; + 'Abacus', + 'Accordion', + 'Acorn', + 'Acrobat', + 'Adult', + 'Aeroplane', + 'Air', + 'Airplane', + 'Alarm', + 'Albatross', + 'Algae', + 'Almond', + 'Alphabet', + 'Ambulance', + 'America', + 'Anatomy', + 'Anchor', + 'Andromeda', + 'Angel', + 'Ant', + 'Apartment', + 'Apology', + 'Apparatus', + 'Apparel', + 'Appeal', + 'Appendix', + 'Apple', + 'Apricot', + 'Aquarium', + 'Arc', + 'Archipelago', + 'Architecture', + 'Argument', + 'Armadillo', + 'Armchair', + 'Armor', + 'Army', + 'Arrow', + 'Artichoke', + 'Artist', + 'Astronaut', + 'Asylum', + 'Athlete', + 'Atmosphere', + 'Atom', + 'Audience', + 'Aurora', + 'Author', + 'Automobile', + 'Autumn', + 'Avalanche', + 'Avenue', + 'Avocado', + 'Award', + 'Awkwardness', + 'Axe', + 'Baboon', + 'Baby', + 'Backbone', + 'Backpack', + 'Bacon', + 'Badger', + 'Bag', + 'Baggage', + 'Bagpipe', + 'Bail', + 'Bait', + 'Baker', + 'Balance', + 'Balcony', + 'Ball', + 'Balloon', + 'Ballot', + 'Bamboo', + 'Banana', + 'Bandana', + 'Bandit', + 'Bangle', + 'Banyan', + 'Bar', + 'Barbecue', + 'Barber', + 'Bargain', + 'Bark', + 'Barley', + 'Barn', + 'Barrel', + 'Basement', + 'Basket', + 'Basketball', + 'Bassoon', + 'Bat', + 'Bath', + 'Battery', + 'Battle', + 'Bay', + 'Beach', + 'Bead', + 'Beak', + 'Beam', + 'Bean', + 'Bear', + 'Beard', + 'Beast', + 'Beat', + 'Beauty', + 'Beaver', + 'Bed', + 'Bedroom', + 'Bee', + 'Beech', + 'Beef', + 'Beer', + 'Beetle', + 'Beggar', + 'Beginner', + 'Beginning', + 'Behavior', + 'Bell', + 'Bellows', + 'Belt', + 'Bench', + 'Bicycle', + 'Bigfoot', + 'Bike', + 'Bill', + 'Bird', + 'Birth', + 'Biscuit', + 'Bit', + 'Blackbird', + 'Blackboard', + 'Blackcurrant', + 'Bladder', + 'Blade', + 'Blanket', + 'Blazer', + 'Blizzard', + 'Block', + 'Blood', + 'Blouse', + 'Blueberry', + 'Boar', + 'Board', + 'Boat', + 'Body', + 'Bologna', + 'Bolt', + 'Bomb', + 'Bone', + 'Bonfire', + 'Bonnet', + 'Book', + 'Bookcase', + 'Boomerang', + 'Boot', + 'Border', + 'Bottle', + 'Bottom', + 'Boulder', + 'Bouquet', + 'Bow', + 'Bowl', + 'Bowler', + 'Box', + 'Boy', + 'Brain', + 'Brake', + 'Branch', + 'Brass', + 'Bread', + 'Breast', + 'Breath', + 'Breeze', + 'Brick', + 'Bridge', + 'Briefcase', + 'Brilliance', + 'Broccoli', + 'Bronze', + 'Brother', + 'Brownie', + 'Brush', + 'Bubble', + 'Bucket', + 'Buffalo', + 'Bug', + 'Building', + 'Bulb', + 'Bull', + 'Bumblebee', + 'Bunker', + 'Bureau', + 'Burger', + 'Burglary', + 'Bus', + 'Bush', + 'Business', + 'Bust', + 'Butter', + 'Butterfly', + 'Button', + 'Buzzard', + 'Cabbage', + 'Cabin', + 'Cabinet', + 'Cable', + 'Cactus', + 'Cadet', + 'Cafe', + 'Cage', + 'Cake', + 'Calculator', + 'Calf', + 'Calm', + 'Camel', + 'Camera', + 'Camp', + 'Can', + 'Canal', + 'Candle', + 'Candy', + 'Cane', + 'Cannibal', + 'Cannon', + 'Canoe', + 'Canyon', + 'Cap', + 'Cape', + 'Capital', + 'Capitalism', + 'Capitol', + 'Captain', + 'Car', + 'Caravan', + 'Carbon', + 'Card', + 'Cardboard', + 'Care', + 'Career', + 'Cargo', + 'Carpet', + 'Carrot', + 'Cart', + 'Carton', + 'Cartoon', + 'Case', + 'Cash', + 'Castle', + 'Cat', + 'Caterpillar', + 'Cathedral', + 'Cattle', + 'Cave', + 'Cavern', + 'CD', + 'Cedar', + 'Cell', + 'Cellar', + 'Cello', + 'Cellophane', + 'Cement', + 'Cemetery', + 'Cereal', + 'Chain', + 'Chair', + 'Chalk', + 'Challenge', + 'Chamber', + 'Champagne', + 'Champion', + 'Chance', + 'Channel', + 'Chaos', + 'Chapel', + 'Chapter', + 'Character', + 'Charity', + 'Charm', + 'Chart', + 'Chase', + 'Chateau', + 'Cheese', + 'Cheetah', + 'Chef', + 'Chemistry', + 'Cherry', + 'Chess', + 'Chest', + 'Chicken', + 'Child', + 'Chimpanzee', + 'Chin', + 'Chip', + 'Chocolate', + 'Choice', + 'Choir', + 'Chowder', + 'Christmas', + 'Chrome', + 'Church', + 'Cider', + 'Cigar', + 'Cigarette', + 'Cinema', + 'Circle', + 'Circus', + 'City', + 'Clam', + 'Clarity', + 'Class', + 'Classic', + 'Classroom', + 'Claw', + 'Clay', + 'Cleaner', + 'Clearing', + 'Cleavage', + 'Clerk', + 'Cliff', + 'Climate', + 'Climax', + 'Climbing', + 'Clinic', + 'Clip', + 'Clock', + 'Closet', + 'Cloth', + 'Clothes', + 'Cloud', + 'Clover', + 'Club', + 'Coach', + 'Coal', + 'Coast', + 'Coat', + 'Cobra', + 'Cobweb', + 'Cocktail', + 'Cocoa', + 'Coffee', + 'Coffin', + 'Coin', + 'Cola', + 'Cold', + 'Collar', + 'Collection', + 'College', + 'Colony', + 'Color', + 'Column', + 'Comb', + 'Comet', + 'Comfort', + 'Comic', + 'Command', + 'Comment', + 'Commerce', + 'Committee', + 'Common', + 'Communication', + 'Community', + 'Company', + 'Compass', + 'Competition', + 'Composer', + 'Compromise', + 'Computer', + 'Concentration', + 'Concept', + 'Concern', + 'Concert', + 'Conclusion', + 'Concrete', + 'Condition', + 'Conduct', + 'Cone', + 'Conference', + 'Confession', + 'Confidence', + 'Conflict', + 'Confusion', + 'Congo', + 'Congress', + 'Conifer', + 'Connection', + 'Consequence', + 'Conservatory', + 'Consideration', + 'Consolation', + 'Conspiracy', + 'Constant', + 'Construction', + 'Consul', + 'Container', + 'Content', + 'Contention', + 'Continent', + 'Contract', + 'Control', + 'Cook', + 'Cooperation', + 'Copper', + 'Copy', + 'Coral', + 'Cord', + 'Cork', + 'Corn', + 'Corner', + 'Coronation', + 'Corporation', + 'Corridor', + 'Corruption', + 'Cosmos', + 'Cost', + 'Costume', + 'Cottage', + 'Cotton', + 'Cough', + 'Council', + 'Count', + 'Counter', + 'Country', + 'Courage', + 'Course', + 'Court', + 'Cousin', + 'Covenant', + 'Cover', + 'Cow', + 'Cowboy', + 'Crab', + 'Cracker', + 'Cradle', + 'Craft', + 'Crane', + 'Crate', + 'Crater', + 'Crayon', + 'Cream', + 'Creativity', + 'Creature', + 'Creek', + 'Crepe', + 'Crib', + 'Cricket', + 'Crime', + 'Crisis', + 'Criterion', + 'Criticism', + 'Crocodile', + 'Crop', + 'Cross', + 'Crow', + 'Crown', + 'Cruiser', + 'Crumb', + 'Crush', + 'Cry', + 'Crystal', + 'Cube', + 'Cuckoo', + 'Cucumber', + 'Cult', + 'Culture', + 'Cup', + 'Cupboard', + 'Curiosity', + 'Curtain', + 'Cushion', + 'Custom', + 'Cut', + 'Cutter', + 'Cycle', + 'Cylinder', + 'Cymbal', + 'Cypress', + 'Daddy', + 'Daffodil', + 'Dagger', + 'Dahlia', + 'Dairy', + 'Dam', + 'Damage', + 'Dance', + 'Danger', + 'Dare', + 'Darkness', + 'Dart', + 'Dash', + 'Database', + 'Date', + 'Daughter', + 'Dawn', + 'Day', + 'Daylight', + 'Deacon', + 'Deal', + 'Dean', + 'Death', + 'Debate', + 'Debt', + 'Decade', + 'Decay', + 'Deception', + 'Decision', + 'Deck', + 'Decline', + 'Decoration', + 'Decree', + 'Deer', + 'Defence', + 'Deficit', + 'Definition', + 'Degree', + 'Delay', + 'Delegation', + 'Delight', + 'Delivery', + 'Demand', + 'Democracy', + 'Demon', + 'Den', + 'Dentist', + 'Department', + 'Departure', + 'Deposit', + 'Depression', + 'Depth', + 'Deputy', + 'Desert', + 'Desire', + 'Desk', + 'Despair', + 'Dessert', + 'Destination', + 'Detail', + 'Detective', + 'Determinant', + 'Development', + 'Device', + 'Devotion', + 'Dew', + 'Diagram', + 'Dialect', + 'Diamond', + 'Diaphragm', + 'Diary', + 'Dictator', + 'Dictionary', + 'Dinosaur', + 'Diploma', + 'Direction', + 'Director', + 'Dirt', + 'Disability', + 'Disadvantage', + 'Disaster', + 'Disc', + 'Discipline', + 'Disclosure', + 'Discount', + 'Discovery', + 'Discussion', + 'Disease', + 'Dish', + 'Disk', + 'Disposal', + 'Distance', + 'Distinction', + 'Disturbance', + 'Dive', + 'Diversity', + 'Division', + 'Doctor', + 'Doctrine', + 'Document', + 'Dog', + 'Dollar', + 'Doll', + 'Dolphin', + 'Donation', + 'Donkey', + 'Door', + 'Doorknob', + 'Dot', + 'Doubt', + 'Dough', + 'Dove', + 'Downpour', + 'Dragon', + 'Drain', + 'Drama', + 'Drawer', + 'Dream', + 'Dressing', + 'Drill', + 'Drink', + 'Drive', + 'Driver', + 'Driving', + 'Drizzle', + 'Drone', + 'Drop', + 'Drum', + 'Duck', + 'Dune', + 'Dungeon', + 'Durian', + 'Dust', + 'Duty', + 'Dwarf', + 'Dye', + 'Eagle', + 'Ear', + 'Earth', + 'Earthquake', + 'Ease', + 'East', + 'Echo', + 'Economy', + 'Edge', + 'Edifice', + 'Editor', + 'Education', + 'Effect', + 'Effort', + 'Egg', + 'Eggplant', + 'Ego', + 'Elbow', + 'Election', + 'Electricity', + 'Elephant', + 'Elevator', + 'Elite', + 'Elk', + 'Embarrassment', + 'Embrace', + 'Emerald', + 'Emotion', + 'Emperor', + 'Emphasis', + 'Empire', + 'Employee', + 'Employer', + 'Employment', + 'End', + 'Endurance', + 'Energy', + 'Engine', + 'Engineer', + 'Enigma', + 'Enjoyment', + 'Enough', + 'Enquiry', + 'Enterprise', + 'Entertainment', + 'Enthusiasm', + 'Entrance', + 'Environment', + 'Episode', + 'Equality', + 'Equation', + 'Equinox', + 'Equipment', + 'Era', + 'Eruption', + 'Escalator', + 'Escape', + 'Espresso', + 'Essence', + 'Estate', + 'Estimate', + 'Eternity', + 'Ethics', + 'Europe', + 'Evaluation', + 'Eve', + 'Evening', + 'Event', + 'Evidence', + 'Evil', + 'Evolution', + 'Example', + 'Excavation', + 'Excellence', + 'Exception', + 'Exchange', + 'Excitement', + 'Excuse', + 'Execution', + 'Executive', + 'Exercise', + 'Exhibition', + 'Existence', + 'Exit', + 'Expansion', + 'Expectation', + 'Expedition', + 'Expense', + 'Experience', + 'Experiment', + 'Expert', + 'Explanation', + 'Exploration', + 'Explosion', + 'Exposure', + 'Extension', + 'Extent', + 'External', + 'Eye', + 'Eyeball', + 'Eyebrow', + 'Eyelash', + 'Eyelid', + 'Fabric', + 'Face', + 'Facility', + 'Fact', + 'Factor', + 'Faculty', + 'Fading', + 'Failure', + 'Fair', + 'Faith', + 'Falcon', + 'Fall', + 'Family', + 'Fan', + 'Fantasy', + 'Farm', + 'Farmer', + 'Fascination', + 'Fashion', + 'Fate', + 'Father', + 'Fatigue', + 'Fault', + 'Fear', + 'Feast', + 'Feather', + 'Feature', + 'Federation', + 'Fee', + 'Feedback', + 'Feeling', + 'Feet', + 'Fence', + 'Ferret', + 'Festival', + 'Fiber', + 'Fiction', + 'Fiddle', + 'Field', + 'Fig', + 'Fight', + 'Figure', + 'File', + 'Film', + 'Filter', + 'Fin', + 'Finance', + 'Finding', + 'Finger', + 'Finish', + 'Fire', + 'Firefighter', + 'Firefly', + 'Firm', + 'Fish', + 'Fisherman', + 'Fitness', + 'Fix', + 'Fixture', + 'Flag', + 'Flame', + 'Flamingo', + 'Flash', + 'Flax', + 'Flea', + 'Fleet', + 'Flesh', + 'Flight', + 'Flock', + 'Flood', + 'Floor', + 'Flour', + 'Flower', + 'Fluid', + 'Flute', + 'Fly', + 'Foam', + 'Fog', + 'Foil', + 'Folder', + 'Folk', + 'Food', + 'Foot', + 'Football', + 'Force', + 'Forecast', + 'Forehead', + 'Forest', + 'Fork', + 'Form', + 'Format', + 'Former', + 'Fort', + 'Fortune', + 'Foundation', + 'Fountain', + 'Fox', + 'Fraction', + 'Fragment', + 'Frame', + 'France', + 'Freedom', + 'Freeze', + 'Freezer', + 'Freight', + 'French', + 'Frequency', + 'Friction', + 'Friend', + 'Frog', + 'Front', + 'Frost', + 'Frown', + 'Fruit', + 'Fuel', + 'Function', + 'Funding', + 'Funeral', + 'Fur', + 'Furnace', + 'Furniture', + 'Future', + 'Gadget', + 'Gaffer', + 'Gain', + 'Gait', + 'Galaxy', + 'Gallery', + 'Galleon', + 'Gallon', + 'Game', + 'Garden', + 'Garlic', + 'Garment', + 'Gas', + 'Gate', + 'Gathering', + 'Gauge', + 'Gazelle', + 'Gear', + 'Geese', + 'Gem', + 'Gender', + 'Gene', + 'General', + 'Genius', + 'Genre', + 'Gentleman', + 'Geography', + 'Geology', + 'Geometry', + 'Geranium', + 'Germany', + 'Gesture', + 'Ghost', + 'Giant', + 'Gift', + 'Giraffe', + 'Girl', + 'Glacier', + 'Gladness', + 'Glass', + 'Glaze', + 'Glimpse', + 'Globe', + 'Gloom', + 'Glory', + 'Glove', + 'Glow', + 'Glue', + 'Goal', + 'Goat', + 'God', + 'Gold', + 'Goldfish', + 'Golf', + 'Gondola', + 'Gong', + 'Good', + 'Goodness', + 'Goose', + 'Gorilla', + 'Gossip', + 'Government', + 'Governor', + 'Gown', + 'Grace', + 'Grade', + 'Grain', + 'Grammar', + 'Grandchild', + 'Grandfather', + 'Grandmother', + 'Grandparent', + 'Grandson', + 'Grape', + 'Graph', + 'Grass', + 'Grasshopper', + 'Gratitude', + 'Grave', + 'Gravity', + 'Grease', + 'Greatness', + 'Greenhouse', + 'Greeting', + 'Grid', + 'Grief', + 'Grille', + 'Grimace', + 'Grind', + 'Grit', + 'Grocery', + 'Ground', + 'Group', + 'Grouse', + 'Growth', + 'Guarantee', + 'Guard', + 'Guess', + 'Guest', + 'Guidance', + 'Guide', + 'Guitar', + 'Gum', + 'Gun', + 'Gutter', + 'Gym', + 'Habitat', + 'Hair', + 'Haircut', + 'Hall', + 'Hamburger', + 'Hammer', + 'Hamster', + 'Hand', + 'Handbag', + 'Handicap', + 'Handle', + 'Handsaw', + 'Happiness', + 'Harbor', + 'Hardship', + 'Hare', + 'Harm', + 'Harmony', + 'Harp', + 'Harvest', + 'Hat', + 'Hate', + 'Hawk', + 'Hazard', + 'Head', + 'Headache', + 'Headland', + 'Headlight', + 'Headline', + 'Headquarters', + 'Health', + 'Hearing', + 'Heart', + 'Heat', + 'Heather', + 'Heaven', + 'Hedge', + 'Hedgehog', + 'Height', + 'Helicopter', + 'Hell', + 'Helmet', + 'Help', + 'Hemisphere', + 'Herb', + 'Heron', + 'Hero', + 'Herring', + 'Hesitation', + 'Hide', + 'Highway', + 'Hill', + 'Hip', + 'Hippopotamus', + 'Hire', + 'Historian', + 'History', + 'Hobbit', + 'Hockey', + 'Hold', + 'Hole', + 'Holiday', + 'Holland', + 'Home', + 'Honey', + 'Honeydew', + 'Honesty', + 'Honor', + 'Hood', + 'Hoof', + 'Hook', + 'Hope', + 'Horizon', + 'Horn', + 'Horse', + 'Hospital', + 'Host', + 'Hostel', + 'Hotdog', + 'Hotel', + 'Hour', + 'House', + 'Hovercraft', + 'Hummingbird', + 'Humor', + 'Hunter', + 'Hurricane', + 'Hurry', + 'Husband', + 'Hut', + 'Hyacinth', + 'Hydrant', + 'Hyena', + 'Hygiene', + 'Hypothesis', + 'Ice', + 'Iceberg', + 'Icicle', + 'Idea', + 'Ideal', + 'Identity', + 'Ideology', + 'Idiot', + 'Igloo', + 'Ignorance', + 'Illusion', + 'Image', + 'Imagination', + 'Impact', + 'Implement', + 'Importance', + 'Impression', + 'Improvement', + 'Incense', + 'Incentive', + 'Inch', + 'Incidence', + 'Income', + 'Increase', + 'Independence', + 'Index', + 'India', + 'Indication', + 'Individual', + 'Industry', + 'Infant', + 'Infection', + 'Influence', + 'Information', + 'Ingenuity', + 'Ingredient', + 'Inhabitant', + 'Inheritance', + 'Initial', + 'Initiative', + 'Injection', + 'Injury', + 'Ink', + 'Inquiry', + 'Insect', + 'Inside', + 'Insight', + 'Insistence', + 'Inspection', + 'Inspector', + 'Inspiration', + 'Installment', + 'Instance', + 'Instinct', + 'Institute', + 'Institution', + 'Instruction', + 'Instrument', + 'Insulation', + 'Insurance', + 'Intelligence', + 'Intention', + 'Interest', + 'Interference', + 'Interior', + 'Internet', + 'Interpretation', + 'Interruption', + 'Interview', + 'Introduction', + 'Intuition', + 'Invention', + 'Inventor', + 'Investment', + 'Invitation', + 'Invoice', + 'Iron', + 'Island', + 'Issue', + 'Italy', + 'Item', + 'Jackal', + 'Jacket', + 'Jaguar', + 'Jail', + 'Jalapeño', + 'Jam', + 'Jar', + 'Jasmine', + 'Javelin', + 'Jaw', + 'Jeans', + 'Jelly', + 'Jellyfish', + 'Jet', + 'Jewel', + 'Jewelry', + 'Job', + 'Jockey', + 'Joker', + 'Journey', + 'Joy', + 'Judge', + 'Judgment', + 'Juice', + 'Jumper', + 'Junction', + 'Jungle', + 'Juniper', + 'Junk', + 'Justice', + 'Kangaroo', + 'Karate', + 'Kayaking', + 'Ketchup', + 'Kettle', + 'Key', + 'Keyboard', + 'Kick', + 'Kid', + 'Kidney', + 'Kilometer', + 'King', + 'Kingdom', + 'Kiss', + 'Kitchen', + 'Kite', + 'Kitten', + 'Knapsack', + 'Knee', + 'Kneel', + 'Knife', + 'Knight', + 'Knitting', + 'Knob', + 'Knock', + 'Knot', + 'Knowledge', + 'Koala', + 'Lab', + 'Label', + 'Labor', + 'Laboratory', + 'Lace', + 'Lack', + 'Ladder', + 'Lad', + 'Lady', + 'Lake', + 'Lamb', + 'Lamp', + 'Lancet', + 'Land', + 'Landscape', + 'Lane', + 'Language', + 'Lantern', + 'Lap', + 'Lapel', + 'Larch', + 'Lard', + 'Lark', + 'Laughter', + 'Launch', + 'Lavender', + 'Law', + 'Lawn', + 'Lawyer', + 'Lead', + 'Leader', + 'Leaf', + 'League', + 'Leak', + 'Leather', + 'Lecture', + 'Leg', + 'Legacy', + 'Legal', + 'Legend', + 'Legislation', + 'Leisure', + 'Lemon', + 'Length', + 'Lentil', + 'Leopard', + 'Letter', + 'Lettuce', + 'Level', + 'Lever', + 'Liability', + 'Library', + 'License', + 'Lid', + 'Life', + 'Lift', + 'Light', + 'Lightning', + 'Lilac', + 'Lily', + 'Limb', + 'Lime', + 'Limit', + 'Line', + 'Linen', + 'Link', + 'Lion', + 'Lip', + 'Lipstick', + 'Liquid', + 'List', + 'Listen', + 'Literacy', + 'Literature', + 'Litter', + 'Liver', + 'Lizard', + 'Llama', + 'Loaf', + 'Lobby', + 'Lobster', + 'Local', + 'Location', + 'Lock', + 'Locker', + 'Locust', + 'Loft', + 'Log', + 'Logic', + 'Lollipop', + 'Loneliness', + 'Longboat', + 'Look', + 'Loop', + 'Lord', + 'Loss', + 'Lot', + 'Lotion', + 'Lounge', + 'Love', + 'Lover', + 'Loyalty', + 'Luck', + 'Luggage', + 'Lullaby', + 'Lunch', + 'Lungs', + 'Lute', + 'Luxury', + 'Lynx', + 'Lyre', + 'Macaroon', + 'Machine', + 'Madness', + 'Magazine', + 'Magic', + 'Magnet', + 'Magnitude', + 'Maiden', + 'Mail', + 'Mailbox', + 'Mainland', + 'Maintenance', + 'Majority', + 'Make', + 'Maker', + 'Malaria', + 'Male', + 'Mall', + 'Man', + 'Management', + 'Manager', + 'Mango', + 'Mansion', + 'Manual', + 'Manufacture', + 'Map', + 'Maple', + 'Marble', + 'March', + 'Mare', + 'Margin', + 'Mark', + 'Market', + 'Marketing', + 'Marmalade', + 'Marriage', + 'Marsh', + 'Marshmallow', + 'Mask', + 'Mass', + 'Master', + 'Match', + 'Material', + 'Math', + 'Matter', + 'Mattress', + 'Maximum', + 'Mayonnaise', + 'Maze', + 'Meadow', + 'Meal', + 'Meaning', + 'Measure', + 'Meat', + 'Mechanic', + 'Mechanism', + 'Medal', + 'Medicine', + 'Mediocrity', + 'Meeting', + 'Melody', + 'Melon', + 'Member', + 'Memo', + 'Memory', + 'Men', + 'Mention', + 'Menu', + 'Mercy', + 'Mess', + 'Message', + 'Metal', + 'Meter', + 'Method', + 'Mexico', + 'Microphone', + 'Microscope', + 'Midday', + 'Middle', + 'Midnight', + 'Might', + 'Migration', + 'Milk', + 'Mill', + 'Millennium', + 'Mind', + 'Mine', + 'Mineral', + 'Minibus', + 'Minimum', + 'Minister', + 'Minority', + 'Mint', + 'Minute', + 'Mirror', + 'Missile', + 'Mission', + 'Mist', + 'Mistake', + 'Mister', + 'Mitten', + 'Mix', + 'Mixture', + 'Mobile', + 'Mode', + 'Model', + 'Modesty', + 'Mole', + 'Moment', + 'Monarch', + 'Money', + 'Monitor', + 'Monkey', + 'Monocle', + 'Monster', + 'Month', + 'Monument', + 'Mood', + 'Moon', + 'Moose', + 'Moral', + 'Morning', + 'Moth', + 'Mother', + 'Motion', + 'Motivation', + 'Motor', + 'Motorcycle', + 'Mound', + 'Mountain', + 'Mouse', + 'Mouth', + 'Movement', + 'Movie', + 'Muffin', + 'Mule', + 'Multiple', + 'Mummy', + 'Muscle', + 'Museum', + 'Mushroom', + 'Music', + 'Musician', + 'Mustard', + 'Mystery', + 'Myth', + 'Nail', + 'Name', + 'Napkin', + 'Nation', + 'Nature', + 'Navel', + 'Navigation', + 'Nebula', + 'Neck', + 'Necklace', + 'Need', + 'Needle', + 'Negative', + 'Negotiation', + 'Neighbor', + 'Neighborhood', + 'Nerve', + 'Nest', + 'Net', + 'Network', + 'News', + 'Newspaper', + 'Niche', + 'Night', + 'Nightmare', + 'Nightingale', + 'Noise', + 'Noodle', + 'North', + 'Nose', + 'Note', + 'Notebook', + 'Nothing', + 'Notice', + 'Notion', + 'Novel', + 'Novelty', + 'Nuisance', + 'Number', + 'Nurse', + 'Nut', + 'Nutrition', + 'Nylon', + 'Oak', + 'Oar', + 'Oasis', + 'Objection', + 'Objective', + 'Obligation', + 'Observation', + 'Observer', + 'Obstacle', + 'Occasion', + 'Occupation', + 'Ocean', + 'Ocelot', + 'Octopus', + 'Offence', + 'Offer', + 'Office', + 'Officer', + 'Official', + 'Oil', + 'Ointment', + 'Old', + 'Olive', + 'Omnibus', + 'Onion', + 'Open', + 'Opening', + 'Opera', + 'Operation', + 'Opinion', + 'Opportunity', + 'Opponent', + 'Optics', + 'Option', + 'Orange', + 'Orchestra', + 'Order', + 'Organ', + 'Organization', + 'Orientation', + 'Origin', + 'Ornament', + 'Ostrich', + 'Otter', + 'Outcome', + 'Outfit', + 'Outlet', + 'Outline', + 'Output', + 'Oval', + 'Oven', + 'Overalls', + 'Overcoat', + 'Overhang', + 'Overload', + 'Owl', + 'Owner', + 'Oxygen', + 'Oyster', + 'Pace', + 'Package', + 'Packet', + 'Pad', + 'Page', + 'Pain', + 'Paint', + 'Painting', + 'Pair', + 'Palace', + 'Paleontologist', + 'Palm', + 'Pan', + 'Panda', + 'Panel', + 'Panic', + 'Pants', + 'Paper', + 'Paperback', + 'Parade', + 'Paradise', + 'Paragraph', + 'Parallel', + 'Parasite', + 'Parent', + 'Park', + 'Parrot', + 'Part', + 'Partner', + 'Party', + 'Passage', + 'Past', + 'Pastor', + 'Patch', + 'Path', + 'Patience', + 'Patient', + 'Patio', + 'Patrol', + 'Pattern', + 'Pause', + 'Pavement', + 'Paw', + 'Payment', + 'Pea', + 'Peace', + 'Peach', + 'Peak', + 'Pear', + 'Pearl', + 'Peasant', + 'Pebble', + 'Pedal', + 'Pedestrian', + 'Peel', + 'Pen', + 'Penalty', + 'Pencil', + 'Penguin', + 'Pension', + 'People', + 'Pepper', + 'Percentage', + 'Perception', + 'Performance', + 'Period', + 'Permission', + 'Permit', + 'Persistence', + 'Person', + 'Personality', + 'Perspective', + 'Pet', + 'Petition', + 'Phase', + 'Pheasant', + 'Phenomenon', + 'Philosophy', + 'Phone', + 'Photo', + 'Photography', + 'Phrase', + 'Physics', + 'Piano', + 'Pickle', + 'Picture', + 'Pie', + 'Piece', + 'Pig', + 'Pigeon', + 'Pigment', + 'Pile', + 'Pill', + 'Pillar', + 'Pillow', + 'Pilot', + 'Pine', + 'Pineapple', + 'Pink', + 'Pint', + 'Pipe', + 'Pistol', + 'Pit', + 'Pitch', + 'Pizza', + 'Place', + 'Plain', + 'Plan', + 'Plane', + 'Planet', + 'Plant', + 'Plastic', + 'Plate', + 'Platform', + 'Play', + 'Player', + 'Playground', + 'Pleasure', + 'Pledge', + 'Plot', + 'Plug', + 'Plum', + 'Plumber', + 'Pocket', + 'Poem', + 'Poet', + 'Point', + 'Poison', + 'Pole', + 'Police', + 'Policy', + 'Politeness', + 'Poll', + 'Pollen', + 'Polo', + 'Pond', + 'Pony', + 'Pool', + 'Popcorn', + 'Pope', + 'Poplar', + 'Poppy', + 'Popularity', + 'Population', + 'Porcelain', + 'Porch', + 'Porcupine', + 'Port', + 'Porter', + 'Portfolio', + 'Portion', + 'Portrait', + 'Position', + 'Possession', + 'Possibility', + 'Post', + 'Postage', + 'Poster', + 'Pot', + 'Potato', + 'Potential', + 'Pound', + 'Poverty', + 'Powder', + 'Power', + 'Practice', + 'Praise', + 'Pray', + 'Prayer', + 'Preacher', + 'Precedence', + 'Precision', + 'Prediction', + 'Preference', + 'Preparation', + 'Presence', + 'Present', + 'Presentation', + 'Preservation', + 'President', + 'Press', + 'Pressure', + 'Prestige', + 'Prevention', + 'Price', + 'Pride', + 'Priest', + 'Primary', + 'Prime', + 'Prince', + 'Princess', + 'Principal', + 'Principle', + 'Print', + 'Prior', + 'Priority', + 'Prison', + 'Prisoner', + 'Privacy', + 'Prize', + 'Problem', + 'Procedure', + 'Process', + 'Prodigy', + 'Produce', + 'Product', + 'Profession', + 'Professor', + 'Profit', + 'Program', + 'Progress', + 'Project', + 'Promise', + 'Promotion', + 'Proof', + 'Property', + 'Proposal', + 'Prosecution', + 'Prospect', + 'Protection', + 'Protest', + 'Proud', + 'Province', + 'Provision', + 'Psychology', + 'Pub', + 'Public', + 'Publication', + 'Publicity', + 'Publisher', + 'Pudding', + 'Puddle', + 'Puffin', + 'Pull', + 'Pulpit', + 'Pulse', + 'Puma', + 'Pump', + 'Pumpkin', + 'Punch', + 'Punctuation', + 'Punishment', + 'Puppet', + 'Puppy', + 'Purchase', + 'Purpose', + 'Purse', + 'Pursuit', + 'Push', + 'Put', + 'Pyramid', + 'Quadrant', + 'Quail', + 'Quality', + 'Quantity', + 'Quartz', + 'Queen', + 'Quill', + 'Quilt', + 'Quince', + 'Quit', + 'Rabbit', + 'Raccoon', + 'Race', + 'Rack', + 'Radar', + 'Radiator', + 'Radio', + 'Radish', + 'Raft', + 'Rag', + 'Rail', + 'Rain', + 'Rainbow', + 'Raincoat', + 'Ram', + 'Range', + 'Rank', + 'Rapids', + 'Rate', + 'Raven', + 'Raw', + 'Ray', + 'Razor', + 'Reaction', + 'Reader', + 'Reading', + 'Reality', + 'Reason', + 'Reception', + 'Recipe', + 'Recognition', + 'Recommendation', + 'Record', + 'Recording', + 'Recovery', + 'Recreation', + 'Rectangle', + 'Redcurrant', + 'Redoubt', + 'Reed', + 'Reference', + 'Refinement', + 'Reflection', + 'Reform', + 'Refusal', + 'Regard', + 'Regret', + 'Regular', + 'Regulation', + 'Rein', + 'Reindeer', + 'Relation', + 'Relationship', + 'Relaxation', + 'Release', + 'Relief', + 'Religion', + 'Remark', + 'Remedy', + 'Reminder', + 'Removal', + 'Rent', + 'Repair', + 'Repeat', + 'Replacement', + 'Reply', + 'Report', + 'Reporter', + 'Representation', + 'Representative', + 'Reproduction', + 'Reputation', + 'Request', + 'Requirement', + 'Research', + 'Reserve', + 'Resident', + 'Resistance', + 'Resolution', + 'Resort', + 'Resource', + 'Respect', + 'Response', + 'Responsibility', + 'Rest', + 'Restaurant', + 'Result', + 'Retail', + 'Retirement', + 'Return', + 'Revelation', + 'Revenge', + 'Revenue', + 'Reverse', + 'Review', + 'Revolution', + 'Reward', + 'Rhetoric', + 'Rhinoceros', + 'Rhythm', + 'Rib', + 'Ribbon', + 'Rice', + 'Riddle', + 'Ridge', + 'Rifle', + 'Right', + 'Ring', + 'Rip', + 'Rise', + 'Risk', + 'Ritual', + 'River', + 'Road', + 'Roadrunner', + 'Roast', + 'Robbery', + 'Robot', + 'Rock', + 'Rocket', + 'Rodent', + 'Role', + 'Roll', + 'Romance', + 'Roof', + 'Room', + 'Rooster', + 'Root', + 'Rope', + 'Rose', + 'Rotation', + 'Rough', + 'Round', + 'Route', + 'Routine', + 'Row', + 'Royal', + 'Rubbish', + 'Ruby', + 'Rucksack', + 'Ruin', + 'Rule', + 'Ruler', + 'Rumor', + 'Run', + 'Runner', + 'Rush', + 'Rust', + 'Rye', + 'Sack', + 'Sacrifice', + 'Sadness', + 'Saddle', + 'Safari', + 'Safety', + 'Saffron', + 'Sage', + 'Sail', + 'Sailor', + 'Salad', + 'Salary', + 'Sale', + 'Salmon', + 'Salt', + 'Sample', + 'Sand', + 'Sandal', + 'Sandwich', + 'Sapphire', + 'Satellite', + 'Satisfaction', + 'Sauce', + 'Saucepan', + 'Sausage', + 'Save', + 'Saw', + 'Scale', + 'Scandal', + 'Scarecrow', + 'Scarf', + 'Scene', + 'Schedule', + 'Scheme', + 'Scholar', + 'Scholarship', + 'School', + 'Science', + 'Scientist', + 'Scoop', + 'Scope', + 'Score', + 'Scorpion', + 'Scout', + 'Scrabble', + 'Scramble', + 'Scrap', + 'Scratch', + 'Screen', + 'Screw', + 'Script', + 'Sculpture', + 'Sea', + 'Seagull', + 'Seal', + 'Search', + 'Seashore', + 'Season', + 'Seat', + 'Secret', + 'Secretary', + 'Section', + 'Sector', + 'Security', + 'Seed', + 'Seek', + 'Selection', + 'Self', + 'Seller', + 'Sense', + 'Sensitivity', + 'Sentence', + 'Separation', + 'September', + 'Sequence', + 'Series', + 'Serve', + 'Server', + 'Service', + 'Session', + 'Set', + 'Setting', + 'Settle', + 'Settlement', + 'Shade', + 'Shadow', + 'Shake', + 'Shame', + 'Shampoo', + 'Shape', + 'Share', + 'Shark', + 'Sheep', + 'Sheet', + 'Shelf', + 'Shell', + 'Shelter', + 'Shepherd', + 'Shield', + 'Shift', + 'Shine', + 'Ship', + 'Shirt', + 'Shock', + 'Shoe', + 'Shooting', + 'Shop', + 'Shopping', + 'Shore', + 'Short', + 'Shortage', + 'Shot', + 'Shoulder', + 'Shout', + 'Show', + 'Shower', + 'Shrimp', + 'Shrine', + 'Sibling', + 'Sick', + 'Side', + 'Sidewalk', + 'Sight', + 'Sign', + 'Signal', + 'Signature', + 'Significance', + 'Silence', + 'Silk', + 'Silver', + 'Similarity', + 'Simon', + 'Simple', + 'Simplicity', + 'Sin', + 'Singer', + 'Single', + 'Sink', + 'Sister', + 'Site', + 'Situation', + 'Size', + 'Sketch', + 'Ski', + 'Skill', + 'Skin', + 'Skirt', + 'Skull', + 'Sky', + 'Slab', + 'Slang', + 'Slave', + 'Slavery', + 'Sleep', + 'Sleeve', + 'Slice', + 'Slide', + 'Slip', + 'Slope', + 'Slot', + 'Sly', + 'Smell', + 'Smile', + 'Smoke', + 'Snail', + 'Snake', + 'Snow', + 'Soap', + 'Society', + 'Sock', + 'Soda', + 'Softness', + 'Soil', + 'Solar', + 'Soldier', + 'Sole', + 'Solicitor', + 'Solid', + 'Solution', + 'Somersault', + 'Son', + 'Song', + 'Soprano', + 'Sore', + 'Sort', + 'Soul', + 'Sound', + 'Soup', + 'Source', + 'South', + 'Sovereignty', + 'Space', + 'Spade', + 'Spaghetti', + 'Spain', + 'Span', + 'Sparrow', + 'Speaker', + 'Special', + 'Specialist', + 'Species', + 'Specification', + 'Speech', + 'Speed', + 'Spell', + 'Spice', + 'Spider', + 'Spike', + 'Spinach', + 'Spirit', + 'Spite', + 'Splash', + 'Spleen', + 'Splendor', + 'Split', + 'Spoon', + 'Sport', + 'Spot', + 'Spring', + 'Squad', + 'Square', + 'Squash', + 'Squirrel', + 'Stability', + 'Stadium', + 'Staff', + 'Stage', + 'Stain', + 'Stair', + 'Stake', + 'Stall', + 'Stamp', + 'Stance', + 'Stand', + 'Standard', + 'Star', + 'Stare', + 'Start', + 'State', + 'Statement', + 'Station', + 'Statistic', + 'Status', + 'Steak', + 'Steam', + 'Steel', + 'Steering', + 'Step', + 'Stick', + 'Sticker', + 'Stomach', + 'Stone', + 'Stool', + 'Stop', + 'Storage', + 'Store', + 'Storm', + 'Story', + 'Stove', + 'Strain', + 'Strait', + 'Strand', + 'Stranger', + 'Strategy', + 'Strawberry', + 'Stream', + 'Street', + 'Strength', + 'Stress', + 'Stretch', + 'Strike', + 'String', + 'Strip', + 'Structure', + 'Struggle', + 'Student', + 'Studio', + 'Study', + 'Stuff', + 'Stupidity', + 'Style', + 'Subject', + 'Submission', + 'Substance', + 'Success', + 'Sugar', + 'Suggestion', + 'Suit', + 'Sulphur', + 'Summit', + 'Sun', + 'Sunshine', + 'Supermarket', + 'Supply', + 'Support', + 'Surgeon', + 'Surgery', + 'Surname', + 'Surprise', + 'Surrender', + 'Surroundings', + 'Survey', + 'Survival', + 'Suspicion', + 'Swallow', + 'Swan', + 'Swear', + 'Sweater', + 'Sweep', + 'Sweet', + 'Sweetness', + 'Swim', + 'Swimming', + 'Swine', + 'Swing', + 'Switch', + 'Sword', + 'Symbol', + 'Sympathy', + 'Syrup', + 'System', + 'Table', + 'Tablet', + 'Taboo', + 'Tackle', + 'Tail', + 'Tailor', + 'Talent', + 'Talk', + 'Tank', + 'Tap', + 'Tape', + 'Target', + 'Task', + 'Taste', + 'Tattoo', + 'Tax', + 'Taxi', + 'Tea', + 'Teacher', + 'Team', + 'Tear', + 'Technique', + 'Technology', + 'Teeth', + 'Telegram', + 'Telephone', + 'Telescope', + 'Television', + 'Temper', + 'Temperature', + 'Temple', + 'Tempo', + 'Temptation', + 'Tendency', + 'Tennis', + 'Tension', + 'Tent', + 'Term', + 'Territory', + 'Terror', + 'Test', + 'Text', + 'Texture', + 'Thank', + 'Thanks', + 'Theme', + 'Theory', + 'Therapy', + 'Thick', + 'Thief', + 'Thing', + 'Think', + 'Thinness', + 'Thought', + 'Thread', + 'Threat', + 'Throat', + 'Throne', + 'Thumb', + 'Thunder', + 'Ticket', + 'Tide', + 'Tiger', + 'Tile', + 'Time', + 'Tin', + 'Tip', + 'Tire', + 'Tissue', + 'Title', + 'Toad', + 'Toast', + 'Tobacco', + 'Today', + 'Toe', + 'Together', + 'Toilet', + 'Toleration', + 'Tomato', + 'Tomorrow', + 'Tongue', + 'Tonight', + 'Tool', + 'Tooth', + 'Toothbrush', + 'Toothpaste', + 'Top', + 'Topic', + 'Torch', + 'Tornado', + 'Tortoise', + 'Torture', + 'Total', + 'Touch', + 'Tough', + 'Tour', + 'Tourism', + 'Tourist', + 'Towel', + 'Tower', + 'Town', + 'Toxic', + 'Toy', + 'Trace', + 'Track', + 'Tractor', + 'Trade', + 'Tradition', + 'Traffic', + 'Tragedy', + 'Trail', + 'Trailer', + 'Train', + 'Trainer', + 'Training', + 'Trait', + 'Transaction', + 'Transcript', + 'Transfer', + 'Transformation', + 'Transition', + 'Translation', + 'Transmission', + 'Transport', + 'Trap', + 'Travel', + 'Tray', + 'Treasure', + 'Treat', + 'Treatment', + 'Tree', + 'Trial', + 'Triangle', + 'Tribe', + 'Trick', + 'Trolley', + 'Trouble', + 'Trousers', + 'Truck', + 'Trumpet', + 'Trunk', + 'Trust', + 'Truth', + 'Tuba', + 'Tube', + 'Tulip', + 'Tuna', + 'Tunnel', + 'Turkey', + 'Turn', + 'Turnip', + 'Turtle', + 'Twin', + 'Twist', + 'Type', + 'Typhoon', + 'Tyre', + 'Uganda', + 'Ukulele', + 'Ultimate', + 'Umbrella', + 'Uncle', + 'Under', + 'Underpants', + 'Understanding', + 'Underwear', + 'Unemployment', + 'Uniform', + 'Union', + 'Unit', + 'United', + 'Unity', + 'Universe', + 'University', + 'Unknown', + 'Unrest', + 'Uranus', + 'Urgent', + 'Urine', + 'Usage', + 'Use', + 'User', + 'Utility', + 'Utopia', + 'Vacancy', + 'Vacation', + 'Vacuum', + 'Valley', + 'Value', + 'Van', + 'Vanilla', + 'Vanish', + 'Variety', + 'Vase', + 'Vastness', + 'Veal', + 'Vector', + 'Vegetable', + 'Vehicle', + 'Veil', + 'Vein', + 'Velvet', + 'Vendor', + 'Ventilation', + 'Venture', + 'Verdict', + 'Verification', + 'Verse', + 'Version', + 'Vessel', + 'Veteran', + 'Vet', + 'Vibration', + 'Vicinity', + 'Victim', + 'Victory', + 'Video', + 'View', + 'Village', + 'Vine', + 'Vinegar', + 'Violation', + 'Violet', + 'Violin', + 'Viper', + 'Virus', + 'Visa', + 'Vision', + 'Visit', + 'Visitor', + 'Visual', + 'Vitality', + 'Vitamin', + 'Vocation', + 'Vodka', + 'Voice', + 'Volcano', + 'Volleyball', + 'Volume', + 'Volunteer', + 'Vomit', + 'Vortex', + 'Vote', + 'Voucher', + 'Vowel', + 'Voyage', + 'Vulture', + 'Waddle', + 'Wafer', + 'Wage', + 'Wagon', + 'Waist', + 'Waiter', + 'Wake', + 'Walk', + 'Wall', + 'Wallet', + 'Walnut', + 'Waltz', + 'War', + 'Ward', + 'Warehouse', + 'Warmth', + 'Warning', + 'Warrant', + 'Warrior', + 'Wash', + 'Washer', + 'Waste', + 'Watch', + 'Water', + 'Waterfall', + 'Wave', + 'Wavelength', + 'Wax', + 'Way', + 'Weakness', + 'Wealth', + 'Weapon', + 'Weather', + 'Web', + 'Wedding', + 'Wedge', + 'Week', + 'Weekend', + 'Weight', + 'Welcome', + 'Welfare', + 'Well', + 'West', + 'Whale', + 'Wheat', + 'Wheel', + 'Whiskey', + 'Whisper', + 'Whistle', + 'White', + 'Whole', + 'Wicket', + 'Widow', + 'Wife', + 'Wild', + 'Wilderness', + 'Wildlife', + 'Will', + 'Willow', + 'Win', + 'Wind', + 'Windmill', + 'Window', + 'Wine', + 'Wing', + 'Winner', + 'Winter', + 'Wire', + 'Wisdom', + 'Wish', + 'Witness', + 'Wolf', + 'Woman', + 'Wonder', + 'Wood', + 'Wool', + 'Word', + 'Work', + 'Worker', + 'World', + 'Worm', + 'Worry', + 'Worship', + 'Worth', + 'Wound', + 'Wrap', + 'Wreck', + 'Wrestler', + 'Wrinkle', + 'Wrist', + 'Writer', + 'Writing', + 'Xenon', + 'X-ray', + 'Xylophone', + 'Yacht', + 'Yak', + 'Yam', + 'Yard', + 'Yarn', + 'Yawn', + 'Year', + 'Yeast', + 'Yellow', + 'Yen', + 'Yoga', + 'Yogurt', + 'Yolk', + 'Young', + 'Youth', + 'Yukon', + 'Zebra', + 'Zenith', + 'Zephyr', + 'Zero', + 'Zigzag', + 'Zinc', + 'Zinnia', + 'Zip', + 'Zone', + 'Zoo', + 'Zoology', + 'Zoom' +] From 4ce579484c97c35aae536b44da29dfa1749549ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 18:02:19 +0200 Subject: [PATCH 20/22] remove doc --- .github/workflows/ci.yml | 8 -------- package.json | 2 -- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 722545e..2bab912 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,14 +18,6 @@ jobs: uses: ./.github/actions/setup.node - name: Lint run: pnpm lint - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup - uses: ./.github/actions/setup.node - - name: Generate docs - run: pnpm doc test: runs-on: ubuntu-latest needs: lint diff --git a/package.json b/package.json index e5b78b3..0eac7bd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ }, "scripts": { "lint": "eslint src bin", - "doc": "jsdoc -c .jsdoc.js src", "test": "node --test test/spec/**/*.test.ts", "test:e2e": "node --test test/e2e/**/*.test.ts | node scripts/taplog.ts utap \"pino-pretty -c\"", "start": "node bin/noray.ts | pino-pretty", @@ -36,7 +35,6 @@ "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0", "eslint-plugin-promise": "^6.0.0", - "jsdoc": "^4.0.2", "pino-pretty": "^10.0.0", "sinon": "^15.0.4", "typescript": "^6.0.3", From 75c1edff3aae1ed77f87b4394ac057add1a9e008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Sun, 7 Jun 2026 18:04:39 +0200 Subject: [PATCH 21/22] pkglok --- package.json | 2 +- pnpm-lock.yaml | 308 +++---------------------------------------------- 2 files changed, 18 insertions(+), 292 deletions(-) diff --git a/package.json b/package.json index 0eac7bd..b654e95 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "eslint-plugin-n": "^15.0.0", "eslint-plugin-promise": "^6.0.0", "pino-pretty": "^10.0.0", - "sinon": "^15.0.4", + "sinon": "^22.0.0", "typescript": "^6.0.3", "typescript-eslint": "^8.60.1", "utap": "^0.2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a61da6d..910876e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,6 @@ importers: eslint-plugin-promise: specifier: ^6.0.0 version: 6.6.0(eslint@10.4.1) - jsdoc: - specifier: ^4.0.2 - version: 4.0.4 node: specifier: runtime:24.x version: runtime:24.16.0 @@ -55,8 +52,8 @@ importers: specifier: ^10.0.0 version: 10.3.1 sinon: - specifier: ^15.0.4 - version: 15.2.0 + specifier: ^22.0.0 + version: 22.0.0 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -69,23 +66,6 @@ importers: packages: - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.27.4': - resolution: {integrity: sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/types@7.27.3': - resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} - engines: {node: '>=6.9.0'} - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -156,30 +136,17 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@jsdoc/salty@0.2.9': - resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==} - engines: {node: '>=v12.0.0'} - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@sinonjs/fake-timers@11.3.1': - resolution: {integrity: sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==} + '@sinonjs/fake-timers@15.4.0': + resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} - '@sinonjs/samsam@8.0.2': - resolution: {integrity: sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==} - - '@sinonjs/text-encoding@0.7.3': - resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} - deprecated: |- - Deprecated: no longer maintained and no longer used by Sinon packages. See - https://github.com/sinonjs/nise/issues/243 for replacement details. + '@sinonjs/samsam@10.0.2': + resolution: {integrity: sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==} '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -193,15 +160,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@typescript-eslint/eslint-plugin@8.60.1': resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -278,9 +236,6 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -330,9 +285,6 @@ packages: bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -358,10 +310,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - catharsis@0.9.0: - resolution: {integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==} - engines: {node: '>= 10'} - chalk@5.4.1: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -419,8 +367,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} doctrine@2.1.0: @@ -438,10 +386,6 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -470,10 +414,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -688,17 +628,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -846,14 +779,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js2xmlparser@4.0.2: - resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} - - jsdoc@4.0.4: - resolution: {integrity: sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==} - engines: {node: '>=12.0.0'} - hasBin: true - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -867,55 +792,21 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true - just-extend@6.2.0: - resolution: {integrity: sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - klaw@3.0.0: - resolution: {integrity: sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - markdown-it-anchor@8.6.7: - resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} - peerDependencies: - '@types/markdown-it': '*' - markdown-it: '*' - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - - marked@4.3.0: - resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} - engines: {node: '>= 12'} - hasBin: true - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -926,11 +817,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -942,9 +828,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - nise@5.1.9: - resolution: {integrity: sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==} - node@runtime:24.16.0: resolution: type: variations @@ -1124,9 +1007,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -1167,10 +1047,6 @@ packages: pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1198,9 +1074,6 @@ packages: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} - requizzle@0.2.4: - resolution: {integrity: sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==} - resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -1278,9 +1151,8 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - sinon@15.2.0: - resolution: {integrity: sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==} - deprecated: 16.1.1 + sinon@22.0.0: + resolution: {integrity: sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==} sonic-boom@3.8.1: resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} @@ -1316,10 +1188,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1383,16 +1251,10 @@ packages: engines: {node: '>=14.17'} hasBin: true - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - underscore@1.13.7: - resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1429,28 +1291,12 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - xmlcreate@2.0.4: - resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} snapshots: - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/parser@7.27.4': - dependencies: - '@babel/types': 7.27.3 - - '@babel/types@7.27.3': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1)': dependencies: eslint: 10.4.1 @@ -1510,32 +1356,21 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@jsdoc/salty@0.2.9': - dependencies: - lodash: 4.17.21 - '@rtsao/scc@1.1.0': {} '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 - '@sinonjs/fake-timers@10.3.0': + '@sinonjs/fake-timers@15.4.0': dependencies: '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers@11.3.1': + '@sinonjs/samsam@10.0.2': dependencies: '@sinonjs/commons': 3.0.1 - - '@sinonjs/samsam@8.0.2': - dependencies: - '@sinonjs/commons': 3.0.1 - lodash.get: 4.4.2 type-detect: 4.1.0 - '@sinonjs/text-encoding@0.7.3': {} - '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -1544,15 +1379,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@2.0.0': {} - '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -1661,8 +1487,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - argparse@2.0.1: {} - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -1727,8 +1551,6 @@ snapshots: bintrees@1.0.2: {} - bluebird@3.7.2: {} - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -1764,10 +1586,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - catharsis@0.9.0: - dependencies: - lodash: 4.17.21 - chalk@5.4.1: {} colorette@2.0.20: {} @@ -1822,7 +1640,7 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - diff@5.2.0: {} + diff@9.0.0: {} doctrine@2.1.0: dependencies: @@ -1840,8 +1658,6 @@ snapshots: dependencies: once: 1.4.0 - entities@4.5.0: {} - es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -1924,8 +1740,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - escape-string-regexp@2.0.0: {} - escape-string-regexp@4.0.0: {} eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1))(eslint-plugin-n@15.7.0(eslint@10.4.1))(eslint-plugin-promise@6.6.0(eslint@10.4.1))(eslint@10.4.1): @@ -2169,12 +1983,8 @@ snapshots: gopd@1.2.0: {} - graceful-fs@4.2.11: {} - has-bigints@1.1.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -2320,28 +2130,6 @@ snapshots: joycon@3.1.1: {} - js2xmlparser@4.0.2: - dependencies: - xmlcreate: 2.0.4 - - jsdoc@4.0.4: - dependencies: - '@babel/parser': 7.27.4 - '@jsdoc/salty': 0.2.9 - '@types/markdown-it': 14.1.2 - bluebird: 3.7.2 - catharsis: 0.9.0 - escape-string-regexp: 2.0.0 - js2xmlparser: 4.0.2 - klaw: 3.0.0 - markdown-it: 14.1.0 - markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0) - marked: 4.3.0 - mkdirp: 1.0.4 - requizzle: 0.2.4 - strip-json-comments: 3.1.1 - underscore: 1.13.7 - json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -2352,53 +2140,21 @@ snapshots: dependencies: minimist: 1.2.8 - just-extend@6.2.0: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 - klaw@3.0.0: - dependencies: - graceful-fs: 4.2.11 - levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash.get@4.4.2: {} - - lodash@4.17.21: {} - - markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0): - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - marked@4.3.0: {} - math-intrinsics@1.1.0: {} - mdurl@2.0.0: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -2409,22 +2165,12 @@ snapshots: minimist@1.2.8: {} - mkdirp@1.0.4: {} - ms@2.1.3: {} nanoid@4.0.2: {} natural-compare@1.4.0: {} - nise@5.1.9: - dependencies: - '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers': 11.3.1 - '@sinonjs/text-encoding': 0.7.3 - just-extend: 6.2.0 - path-to-regexp: 6.3.0 - node@runtime:24.16.0: {} object-inspect@1.13.4: {} @@ -2495,8 +2241,6 @@ snapshots: path-parse@1.0.7: {} - path-to-regexp@6.3.0: {} - picomatch@4.0.4: {} pino-abstract-transport@1.2.0: @@ -2554,8 +2298,6 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 - punycode.js@2.3.1: {} - punycode@2.3.1: {} quick-format-unescaped@4.0.4: {} @@ -2592,10 +2334,6 @@ snapshots: regexpp@3.2.0: {} - requizzle@0.2.4: - dependencies: - lodash: 4.17.21 - resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -2689,14 +2427,12 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - sinon@15.2.0: + sinon@22.0.0: dependencies: '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers': 10.3.0 - '@sinonjs/samsam': 8.0.2 - diff: 5.2.0 - nise: 5.1.9 - supports-color: 7.2.0 + '@sinonjs/fake-timers': 15.4.0 + '@sinonjs/samsam': 10.0.2 + diff: 9.0.0 sonic-boom@3.8.1: dependencies: @@ -2740,10 +2476,6 @@ snapshots: strip-json-comments@3.1.1: {} - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} tdigest@0.1.2: @@ -2824,8 +2556,6 @@ snapshots: typescript@6.0.3: {} - uc.micro@2.1.0: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -2833,8 +2563,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - underscore@1.13.7: {} - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -2892,6 +2620,4 @@ snapshots: wrappy@1.0.2: {} - xmlcreate@2.0.4: {} - yocto-queue@0.1.0: {} From 73c6810c1fdf2cf4268a47ca000dddf528e4ebd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s?= Date: Mon, 8 Jun 2026 14:07:59 +0200 Subject: [PATCH 22/22] unoptional config fields --- src/assertions.ts | 21 --------------------- src/config.ts | 29 ++++++++++++++++------------- src/utils.ts | 10 ++++++++++ 3 files changed, 26 insertions(+), 34 deletions(-) delete mode 100644 src/assertions.ts diff --git a/src/assertions.ts b/src/assertions.ts deleted file mode 100644 index 251ec97..0000000 --- a/src/assertions.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { fail } from "node:assert"; - -/** - * Ensure param is a valid enum value. - * - * The `enumDef` must have a value that is equal to `value`. - * - * @throws on invalid `value` - */ -export function requireEnum( - value: T, - enumDef: Readonly>, -): T; -export function requireEnum( - value: T, - enumDef: Record, -): T { - return Object.values(enumDef).includes(value) - ? value - : fail("Invalid enum value: " + value); -} diff --git a/src/config.ts b/src/config.ts index b42929d..ac12cdf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,6 +9,7 @@ import { } from "./config.parsers.ts"; import logger, { getLogLevel } from "./logger.ts"; import { urlAlphabet } from "nanoid"; +import { required } from "./utils.ts"; type ConfigEnv = Record; @@ -40,25 +41,27 @@ export function readConfig(env: ConfigEnv) { }, udpRelay: { - ports: ports(env.NORAY_UDP_RELAY_PORTS ?? "49152-51200"), - timeout: duration(env.NORAY_UDP_RELAY_TIMEOUT ?? "30s"), - cleanupInterval: duration(env.NORAY_UDP_RELAY_CLEANUP_INTERVAL ?? "30s"), + ports: required(ports(env.NORAY_UDP_RELAY_PORTS ?? "49152-51200")), + timeout: required(duration(env.NORAY_UDP_RELAY_TIMEOUT ?? "30s")), + cleanupInterval: required( + duration(env.NORAY_UDP_RELAY_CLEANUP_INTERVAL ?? "30s"), + ), registrarPort: number(env.NORAY_UDP_REGISTRAR_PORT) ?? 8809, - maxIndividualTraffic: byteSize( - env.NORAY_UDP_RELAY_MAX_INDIVIDUAL_TRAFFIC ?? "128kb", + maxIndividualTraffic: required( + byteSize(env.NORAY_UDP_RELAY_MAX_INDIVIDUAL_TRAFFIC ?? "128kb"), ), - maxGlobalTraffic: byteSize( - env.NORAY_UDP_RELAY_MAX_GLOBAL_TRAFFIC ?? "1gb", + maxGlobalTraffic: required( + byteSize(env.NORAY_UDP_RELAY_MAX_GLOBAL_TRAFFIC ?? "1gb"), ), - trafficInterval: duration( - env.NORAY_UDP_RELAY_TRAFFIC_INTERVAL ?? "100ms", + trafficInterval: required( + duration(env.NORAY_UDP_RELAY_TRAFFIC_INTERVAL ?? "100ms"), ), - maxLifetimeDuration: duration( - env.NORAY_UDP_RELAY_MAX_LIFETIME_DURATION ?? "4hr", + maxLifetimeDuration: required( + duration(env.NORAY_UDP_RELAY_MAX_LIFETIME_DURATION ?? "4hr"), ), - maxLifetimeTraffic: byteSize( - env.NORAY_UDP_RELAY_MAX_LIFETIME_TRAFFIC ?? "4gb", + maxLifetimeTraffic: required( + byteSize(env.NORAY_UDP_RELAY_MAX_LIFETIME_TRAFFIC ?? "4gb"), ), }, diff --git a/src/utils.ts b/src/utils.ts index 99f2b93..cdd7e0e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -227,3 +227,13 @@ export function generateWordId(wordCount = 3): string { .map(() => words[randomInt(words.length)]) .join(""); } + +export type ErrorProvider = () => Error; + +export function required( + what: T | undefined | null, + errorProvider: ErrorProvider = () => new Error("Undefined value!"), +): T { + if (what === null || what === undefined) throw errorProvider(); + else return what; +}