diff --git a/.gitignore b/.gitignore index f62b7b2..ae8d5e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ .vscode/ .history +.dev # Ignore build folder build/ diff --git a/package-lock.json b/package-lock.json index ff2370e..b6d1e51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,21 @@ "version": "1.2.9", "license": "NPCL-1", "dependencies": { + "@nasriya/atomix": "^1.0.23", + "@nasriya/cachify": "^0.0.9-beta", + "@nasriya/mimex": "^0.0.2-beta", + "@nasriya/overwatch": "^1.1.4", "ejs": "^3.1.10", "ms": "^2.1.3", - "tldts": "^7.0.16" + "tldts": "^7.0.17" }, "devDependencies": { "@nasriya/postbuild": "^1.1.5", "@types/ejs": "^3.1.5", "@types/jest": "^30.0.0", "@types/ms": "^2.1.0", - "@types/node": "^24.6.2", - "ts-jest": "^29.4.4", + "@types/node": "^24.10.0", + "ts-jest": "^29.4.5", "typescript": "^5.9.3" }, "funding": { @@ -958,6 +962,85 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nasriya/atomix": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/@nasriya/atomix/-/atomix-1.0.23.tgz", + "integrity": "sha512-xQ/NA3Bbx/GKEvqVZNcvs2LrJTEJiA8g8NmVCNDertMyx9TCUF4+Z68T2Vs9vDfbs4YWEN5onSAMucgXd/8hkA==", + "license": "NOL-1", + "dependencies": { + "@nasriya/uuidx": "^1.0.3" + }, + "funding": { + "type": "individual", + "url": "https://fund.nasriya.net/" + } + }, + "node_modules/@nasriya/cachify": { + "version": "0.0.9-beta", + "resolved": "https://registry.npmjs.org/@nasriya/cachify/-/cachify-0.0.9-beta.tgz", + "integrity": "sha512-+/5VUAUGSNPe+ghho/98p+n27JRbWHQ8jqmVTjOUSqkN3L2RU6hKwA9ZKVbd4smwNaaxJeI9ZdVBLNT5eyWQMA==", + "license": "NPCL-1", + "dependencies": { + "@nasriya/atomix": "^1.0.23", + "@nasriya/cron": "^1.1.2", + "@nasriya/overwatch": "^1.1.4", + "@nasriya/uuidx": "^1.0.3" + }, + "funding": { + "type": "individual", + "url": "https://fund.nasriya.net/" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.917.0", + "@redis/client": "^5.9.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "@redis/client": { + "optional": true + } + } + }, + "node_modules/@nasriya/cron": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nasriya/cron/-/cron-1.1.2.tgz", + "integrity": "sha512-X6CDFX37OLfTyLxfrhoVb6nEYc71wvhbxamaC8jNBbp68PXHBStLK4okJ+lUbdyag3Gq6M8De2xllcyVHZupHw==", + "license": "NOL-1", + "dependencies": { + "cron-time-generator": "^2.0.3", + "node-cron": "^4.2.0", + "node-schedule": "^2.1.1" + }, + "funding": { + "type": "individual", + "url": "https://fund.nasriya.net/" + } + }, + "node_modules/@nasriya/mimex": { + "version": "0.0.2-beta", + "resolved": "https://registry.npmjs.org/@nasriya/mimex/-/mimex-0.0.2-beta.tgz", + "integrity": "sha512-Z2/dqrC5T096nF5zE8tyqfmzmZ9ne1OzqeAhoRRYx02079UJDjFVi9jUffZFu5h/IWpbBVcJk/+YLjyHh3H0XA==", + "license": "NOL-1", + "funding": { + "type": "individual", + "url": "https://fund.nasriya.net/" + } + }, + "node_modules/@nasriya/overwatch": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@nasriya/overwatch/-/overwatch-1.1.4.tgz", + "integrity": "sha512-W4PrtMqA4e8IfDGY38OrhT4Pb6KJfqpIl19my6mOjZs+00GokQJX98aTAMR0jLvFLD95CyIErPgdCOkQapVnkQ==", + "license": "NOL-1", + "dependencies": { + "@nasriya/atomix": "^1.0.1" + }, + "funding": { + "type": "individual", + "url": "https://fund.nasriya.net/" + } + }, "node_modules/@nasriya/postbuild": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@nasriya/postbuild/-/postbuild-1.1.5.tgz", @@ -973,6 +1056,16 @@ "url": "https://fund.nasriya.net/" } }, + "node_modules/@nasriya/uuidx": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@nasriya/uuidx/-/uuidx-1.0.3.tgz", + "integrity": "sha512-sVw5/R9Gq3eaAkKTmCJ5UbV8vhh+r6auW2xFBZ+xKRx40CBcVNvxRcb2I+n5dP7osMeX5/fY9EgT5mcOCzZGQQ==", + "license": "Nasriya License", + "funding": { + "type": "individual", + "url": "https://fund.nasriya.net/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1321,13 +1414,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", - "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.13.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/stack-utils": { @@ -1810,6 +1903,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cron-time-generator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cron-time-generator/-/cron-time-generator-2.0.3.tgz", + "integrity": "sha512-02Ab5okFEMpcDernEwUXY16hLCryUxATAFGYyzyLymin0xl/udC50LkBFHX+qOeXnwXMRyK+uH4doXzUSpOoQA==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2105,6 +2216,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3180,6 +3306,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3190,6 +3322,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -3309,6 +3450,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3323,6 +3473,20 @@ "dev": true, "license": "MIT" }, + "node_modules/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3710,6 +3874,12 @@ "node": ">=8" } }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3867,21 +4037,21 @@ } }, "node_modules/tldts": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.16.tgz", - "integrity": "sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==", + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", "license": "MIT", "dependencies": { - "tldts-core": "^7.0.16" + "tldts-core": "^7.0.17" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.16.tgz", - "integrity": "sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==", + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", "license": "MIT" }, "node_modules/tmpl": { @@ -3905,9 +4075,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", - "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==", + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3917,7 +4087,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.2", + "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -3958,9 +4128,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -4036,9 +4206,9 @@ } }, "node_modules/undici-types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", - "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index e36b71c..e63bee5 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "build:cjs": "tsc --project tsconfig.cjs.json", "postbuild-init": "postbuild-init", "test": "jest", - "deepTest": "jest --coverage" + "deepTest": "jest --coverage", + "dev": "node ./.dev" }, "maintainers": [ { @@ -73,13 +74,17 @@ "@types/ejs": "^3.1.5", "@types/jest": "^30.0.0", "@types/ms": "^2.1.0", - "@types/node": "^24.6.2", - "ts-jest": "^29.4.4", + "@types/node": "^24.10.0", + "ts-jest": "^29.4.5", "typescript": "^5.9.3" }, "dependencies": { + "@nasriya/atomix": "^1.0.23", + "@nasriya/cachify": "^0.0.9-beta", + "@nasriya/mimex": "^0.0.2-beta", + "@nasriya/overwatch": "^1.1.4", "ejs": "^3.1.10", "ms": "^2.1.3", - "tldts": "^7.0.16" + "tldts": "^7.0.17" } -} \ No newline at end of file +} diff --git a/src/data/currencies.json b/src/data/currencies.ts similarity index 94% rename from src/data/currencies.json rename to src/data/currencies.ts index 3a94da4..c503d24 100644 --- a/src/data/currencies.json +++ b/src/data/currencies.ts @@ -1,4 +1,4 @@ -[ +const currencies = [ "AED", "AFN", "ALL", @@ -159,4 +159,8 @@ "ZAR", "ZMW", "ZWD" -] \ No newline at end of file +] as const; + +export type Currency = typeof currencies[number]; + +export default currencies; \ No newline at end of file diff --git a/src/data/extensions.json b/src/data/extensions.json deleted file mode 100644 index 0c7588d..0000000 --- a/src/data/extensions.json +++ /dev/null @@ -1,272 +0,0 @@ -[ - { - "extension": ".webp", - "description": "Web Picture", - "mime": "image/webp" - }, - { - "extension": ".aac", - "description": "AAC audio", - "mime": "audio/aac" - }, - { - "extension": ".abw", - "description": "AbiWord document", - "mime": "application/x-abiword" - }, - { - "extension": ".arc", - "description": "Archive document (multiple files embedded)", - "mime": "application/x-freearc" - }, - { - "extension": ".avif", - "description": "AVIF image", - "mime": "image/avif" - }, - { - "extension": ".avi", - "description": "AVI: Audio Video Interleave", - "mime": "video/x-msvideo" - }, - { - "extension": ".azw", - "description": "Amazon Kindle eBook format", - "mime": "application/vnd.amazon.ebook" - }, - { - "extension": ".bin", - "description": "Any kind of binary data", - "mime": "application/octet-stream" - }, - { - "extension": ".bmp", - "description": "Windows OS/2 Bitmap Graphics", - "mime": "image/bmp" - }, - { - "extension": ".bz", - "description": "BZip archive", - "mime": "application/x-bzip" - }, - { - "extension": ".bz2", - "description": "BZip2 archive", - "mime": "application/x-bzip2" - }, - { - "extension": ".cda", - "description": "CD audio", - "mime": "application/x-cdf" - }, - { - "extension": ".csh", - "description": "C-Shell script", - "mime": "application/x-csh" - }, - { - "extension": ".css", - "description": "Cascading Style Sheets (CSS)", - "mime": "text/css" - }, - { - "extension": ".csv", - "description": "Comma-separated values (CSV)", - "mime": "text/csv" - }, - { - "extension": ".doc", - "description": "Microsoft Word", - "mime": "application/msword" - }, - { - "extension": ".docx", - "description": "Microsoft Word (OpenXML)", - "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - }, - { - "extension": ".eot", - "description": "MS Embedded OpenType fonts", - "mime": "application/vnd.ms-fontobject" - }, - { - "extension": ".epub", - "description": "Electronic publication (EPUB)", - "mime": "application/epub+zip" - }, - { - "extension": ".gz", - "description": "GZip Compressed Archive", - "mime": "application/gzip" - }, - { - "extension": ".gif", - "description": "Graphics Interchange Format (GIF)", - "mime": "image/gif" - }, - { - "extension": ".htm, .html", - "description": "HyperText Markup Language (HTML)", - "mime": "text/html" - }, - { - "extension": ".ico", - "description": "Icon format", - "mime": "image/vnd.microsoft.icon" - }, - { - "extension": ".ics", - "description": "iCalendar format", - "mime": "text/calendar" - }, - { - "extension": ".jar", - "description": "Java Archive (JAR)", - "mime": "application/java-archive" - }, - { - "extension": ".jpeg, .jpg", - "description": "JPEG images", - "mime": "image/jpeg" - }, - { - "extension": ".js", - "description": "JavaScript", - "mime": "text/javascript" - }, - { - "extension": ".json", - "description": "JSON format", - "mime": "application/json" - }, - { - "extension": ".jsonld", - "description": "JSON-LD format", - "mime": "application/ld+json" - }, - { - "extension": ".mid, .midi", - "description": "Musical Instrument Digital Interface (MIDI)", - "mime": "audio/midi, audio/x-midi" - }, - { - "extension": ".mjs", - "description": "JavaScript module", - "mime": "text/javascript" - }, - { - "extension": ".mp3", - "description": "MP3 audio", - "mime": "audio/mpeg" - }, - { - "extension": ".mp4", - "description": "MP4 video", - "mime": "video/mp4" - }, - { - "extension": ".mpeg", - "description": "MPEG Video", - "mime": "video/mpeg" - }, - { - "extension": ".mpkg", - "description": "Apple Installer Package", - "mime": "application/vnd.apple.installer+xml" - }, - { - "extension": ".odp", - "description": "OpenDocument presentation document", - "mime": "application/vnd.oasis.opendocument.presentation" - }, - { - "extension": ".ods", - "description": "OpenDocument spreadsheet document", - "mime": "application/vnd.oasis.opendocument.spreadsheet" - }, - { - "extension": ".odt", - "description": "OpenDocument text document", - "mime": "application/vnd.oasis.opendocument.text" - }, - { - "extension": ".oga", - "description": "OGG audio", - "mime": "audio/ogg" - }, - { - "extension": ".ogv", - "description": "OGG video", - "mime": "video/ogg" - }, - { - "extension": ".ogx", - "description": "OGG", - "mime": "application/ogg" - }, - { - "extension": ".opus", - "description": "Opus audio", - "mime": "audio/opus" - }, - { - "extension": ".otf", - "description": "OpenType font", - "mime": "font/otf" - }, - { - "extension": ".png", - "description": "Portable Network Graphics", - "mime": "image/png" - }, - { - "extension": ".pdf", - "description": "Adobe Portable Document Format (PDF)", - "mime": "application/pdf" - }, - { - "extension": ".php", - "description": "Hypertext Preprocessor (Personal Home Page)", - "mime": "application/x-httpd-php" - }, - { - "extension": ".ppt", - "description": "Microsoft PowerPoint", - "mime": "application/vnd.ms-powerpoint" - }, - { - "extension": ".pptx", - "description": "Microsoft PowerPoint (OpenXML)", - "mime": "application/vnd.openxmlformats-officedocument.presentationml.presentation" - }, - { - "extension": ".rar", - "description": "RAR archive", - "mime": "application/vnd.rar" - }, - { - "extension": ".rtf", - "description": "Rich Text Format (RTF)", - "mime": "application/rtf" - }, - { - "extension": ".sh", - "description": "Bourne shell script", - "mime": "application/x-sh" - }, - { - "extension": ".svg", - "description": "Scalable Vector Graphics (SVG)", - "mime": "image/svg+xml" - }, - { - "extension": ".tar", - "description": "Tape Archive (TAR)", - "mime": "application/x-tar" - }, - { - "extension": ".tif, .tiff", - "description": "Tagged Image File Format (TIFF)", - "mime": "image/tiff" - } -] \ No newline at end of file diff --git a/src/data/mimes.json b/src/data/mimes.json deleted file mode 100644 index 91a0c08..0000000 --- a/src/data/mimes.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - "audio/aac", - "application/x-abiword", - "application/x-freearc", - "image/avif", - "video/x-msvideo", - "application/vnd.amazon.ebook", - "application/octet-stream", - "image/bmp", - "application/x-bzip", - "application/x-bzip2", - "application/x-cdf", - "application/x-csh", - "text/css", - "text/csv", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-fontobject", - "application/epub+zip", - "application/gzip", - "image/gif", - "text/html", - "image/vnd.microsoft.icon", - "text/calendar", - "application/java-archive", - "image/jpeg", - "text/javascript", - "text/plain", - "application/json", - "application/ld+json", - "audio/midi, audio/x-midi", - "text/javascript", - "audio/mpeg", - "video/mp4", - "video/mpeg", - "application/vnd.apple.installer+xml", - "application/vnd.apple.mpegurl", - "application/vnd.oasis.opendocument.presentation", - "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.oasis.opendocument.text", - "audio/ogg", - "video/ogg", - "application/ogg", - "audio/opus", - "font/otf", - "image/png", - "application/pdf", - "application/x-httpd-php", - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "application/vnd.rar", - "application/rtf", - "application/x-sh", - "image/svg+xml", - "application/x-tar", - "image/tiff" -] \ No newline at end of file diff --git a/src/docs/docs.ts b/src/docs/docs.ts index 172768b..4d1c649 100644 --- a/src/docs/docs.ts +++ b/src/docs/docs.ts @@ -6,6 +6,10 @@ import HyperCloudResponse from '../services/handler/assets/response'; import HyperCloudServer from '../server'; import HTTPError from '../utils/errors/HTTPError'; import ms from 'ms'; +import { Mime } from '@nasriya/mimex'; +import { Currency } from '../data/currencies'; + +export { Currency } from '../data/currencies'; /**The website's possible color schemes */ export type ColorScheme = 'Dark' | 'Light'; @@ -36,42 +40,9 @@ export type PageRenderingCacheAsset = Exclude; export type DeepReadonly = { readonly [P in keyof T]: DeepReadonly; }; -/**A currency code */ -export type Currency = - | 'AED' | 'AFN' | 'ALL' | 'AMD' | 'ANG' | 'AOA' | 'ARS' | 'AUD' | 'AWG' | 'AZN' - | 'BAM' | 'BBD' | 'BDT' | 'BGN' | 'BHD' | 'BIF' | 'BMD' | 'BND' | 'BOB' | 'BRL' - | 'BSD' | 'BTN' | 'BWP' | 'BYN' | 'BZD' | 'CAD' | 'CDF' | 'CHF' | 'CLP' | 'CNY' - | 'COP' | 'CRC' | 'CUP' | 'CVE' | 'CZK' | 'DJF' | 'DKK' | 'DOP' | 'DZD' | 'EGP' - | 'ERN' | 'ETB' | 'EUR' | 'FJD' | 'FKP' | 'FOK' | 'GBP' | 'GEL' | 'GGP' | 'GHS' - | 'GIP' | 'GMD' | 'GNF' | 'GTQ' | 'GYD' | 'HKD' | 'HNL' | 'HRK' | 'HTG' | 'HUF' - | 'IDR' | 'ILS' | 'IMP' | 'INR' | 'IQD' | 'IRR' | 'ISK' | 'JEP' | 'JMD' | 'JOD' - | 'JPY' | 'KES' | 'KGS' | 'KHR' | 'KID' | 'KMF' | 'KRW' | 'KWD' | 'KYD' | 'KZT' - | 'LAK' | 'LBP' | 'LKR' | 'LRD' | 'LSL' | 'LYD' | 'MAD' | 'MDL' | 'MGA' | 'MKD' - | 'MMK' | 'MNT' | 'MOP' | 'MRU' | 'MUR' | 'MVR' | 'MWK' | 'MXN' | 'MYR' | 'MZN' - | 'NAD' | 'NGN' | 'NIO' | 'NOK' | 'NPR' | 'NZD' | 'OMR' | 'PAB' | 'PEN' | 'PGK' - | 'PHP' | 'PKR' | 'PLN' | 'PYG' | 'QAR' | 'RON' | 'RSD' | 'RUB' | 'RWF' | 'SAR' - | 'SBD' | 'SCR' | 'SDG' | 'SEK' | 'SGD' | 'SHP' | 'SLL' | 'SOS' | 'SPL' | 'SRD' - | 'STN' | 'SYP' | 'SZL' | 'THB' | 'TJS' | 'TMT' | 'TND' | 'TOP' | 'TRY' | 'TTD' - | 'TVD' | 'TWD' | 'TZS' | 'UAH' | 'UGX' | 'USD' | 'UYU' | 'UZS' | 'VES' | 'VND' - | 'VUV' | 'WST' | 'XAF' | 'XCD' | 'XOF' | 'XPF' | 'YER' | 'ZAR' | 'ZMW' | 'ZWD'; /**These mime types are used when sending/receiving files */ -export type MimeType = - | "audio/aac" | "application/x-abiword" | "application/x-freearc" | "image/avif" - | "video/x-msvideo" | "application/vnd.amazon.ebook" | "application/octet-stream" - | "image/bmp" | "application/x-bzip" | "application/x-bzip2" | "application/x-cdf" - | "application/x-csh" | "text/calendar" | "text/css" | "text/plain" | "text/csv" | "application/msword" - | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - | "application/vnd.ms-fontobject" | "application/epub+zip" | "application/gzip" - | "image/gif" | "text/html" | "image/vnd.microsoft.icon" | "text/calendar" | "application/java-archive" - | "image/jpeg" | "text/javascript" | "application/json" | "application/ld+json" | "audio/midi" - | "audio/x-midi" | "audio/mpeg" | "video/mp4" | "video/mpeg" | "application/vnd.apple.installer+xml" - | "application/vnd.oasis.opendocument.presentation" | "application/vnd.oasis.opendocument.spreadsheet" - | "application/vnd.oasis.opendocument.text" | "audio/ogg" | "video/ogg" | "application/ogg" - | "audio/opus" | "font/otf" | "image/png" | "application/pdf" | "application/x-httpd-php" - | "application/vnd.ms-powerpoint" | "application/vnd.openxmlformats-officedocument.presentationml.presentation" - | "application/vnd.rar" | "application/rtf" | "application/x-sh" | "image/svg+xml" - | "application/x-tar" | "image/tiff"; +export type MimeType = Mime | 'text/plain'; export type OnRenderHandler = (locals: Record | any, include: (name: string, locals: Record) => Promise, lang: string) => string | Promise; @@ -671,15 +642,13 @@ export interface NotFoundResponseOptions { export interface StaticRouteOptions { /** The route path URL. */ - path: string; + path?: string; /** Option for serving dotfiles. Possible values are `allow`, `deny`, `ignore`. Default: `ignore`. */ dotfiles?: 'allow' | 'ignore' | 'deny'; /** The host's `subDomain` from HyperCloudRequest.subDomain. Default: `null`. */ subDomain?: string; /** This will match only if the `path` exactly matches the HyperCloudRequest.path */ caseSensitive?: boolean; - /** Whether to cache the file in memory. Default: `true` */ - memoryCache?: boolean; } export interface CookieOptions { diff --git a/src/services/cache/routeCache.ts b/src/services/cache/routeCache.ts new file mode 100644 index 0000000..0c489a0 --- /dev/null +++ b/src/services/cache/routeCache.ts @@ -0,0 +1,8 @@ +import cachify from "@nasriya/cachify"; + +const routeCache = cachify.createClient(); + +// On TTL expiration, remove the file content from the cache to save space +routeCache.files.configs.ttl.policy = 'keep'; + +export default routeCache; \ No newline at end of file diff --git a/src/services/handler/assets/response.ts b/src/services/handler/assets/response.ts index f865c5b..ec6bd0e 100644 --- a/src/services/handler/assets/response.ts +++ b/src/services/handler/assets/response.ts @@ -1,3 +1,4 @@ +import mimex, { mimes, Mime } from '@nasriya/mimex'; import path from 'path'; import helpers from '../../../utils/helpers'; import HyperCloudRequest from './request'; @@ -15,11 +16,6 @@ import net from 'net'; import tls from 'tls'; import { NotFoundResponseOptions, ForbiddenAndUnauthorizedOptions, ServerErrorOptions, RedirectCode, DownloadFileOptions, SendFileOptions, MimeType, ExtensionData, NextFunction, PageRenderingOptions } from '../../../docs/docs'; -const _dirname = __dirname; - -const mimes = helpers.loadJSON(path.resolve(_dirname, '../../../data/mimes.json')) as string[]; -const extensions = helpers.loadJSON(path.resolve(_dirname, '../../../data/extensions.json')) as ExtensionData[]; - interface ResponseEndOptions { data?: string | Uint8Array; encoding?: BufferEncoding; @@ -675,7 +671,8 @@ export class HyperCloudResponse { // Preparing the mime-type const exts = fileName.split('.').filter(i => i.length > 0); const extension = `.${exts[exts.length - 1]}`; - const mime = extensions.find(i => i.extension.includes(extension))?.mime as string; + const extMimes = mimex.getMimes(extension); + const mime = extMimes ? extMimes[0] : 'application/octet-stream'; // Check if the download option is triggered or not if (options && 'download' in options) { @@ -771,7 +768,7 @@ export class HyperCloudResponse { let type: MimeType = null as unknown as MimeType; if (typeof data === 'string') { - if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase())) { + if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase() as Mime)) { type = contentType; } else if (helpers.is.html(data)) { type = 'text/html'; @@ -779,14 +776,14 @@ export class HyperCloudResponse { type = 'text/plain'; } } else if (Buffer.isBuffer(data)) { - if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase())) { + if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase() as Mime)) { type = contentType; } else { type = 'application/octet-stream'; } } else if (Array.isArray(data) || (typeof data === 'object' && data !== null)) { data = JSON.stringify(data); - if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase())) { + if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase() as Mime)) { type = contentType; } else { type = 'application/json'; diff --git a/src/services/routes/assets/router.ts b/src/services/routes/assets/router.ts index c348d10..24f0e31 100644 --- a/src/services/routes/assets/router.ts +++ b/src/services/routes/assets/router.ts @@ -1,12 +1,15 @@ +import atomix from '@nasriya/atomix'; import HyperCloudServer from '../../../server'; -import { HttpMethod, HyperCloudRequestHandler, StaticRouteOptions } from '../../../docs/docs'; import Route from './route'; import StaticRoute from './staticRoute'; import helpers from '../../../utils/helpers'; +import type { HttpMethod, HyperCloudRequestHandler, StaticRouteOptions } from '../../../docs/docs'; import fs from 'fs'; import path from 'path'; +const hasOwnProp = atomix.dataTypes.record.hasOwnProperty; + export class Router { #_server: HyperCloudServer | undefined; #_routes = { static: [] as StaticRoute[], dynamic: [] as Route[] } @@ -32,11 +35,16 @@ export class Router { createStaticRoute: (root: string, options?: StaticRouteOptions) => { const caseSensitive = options && 'caseSensitive' in options ? options.caseSensitive : this.#_defaults.caseSensitive; const subDomain = options && 'subDomain' in options ? options.subDomain : this.#_defaults.subDomain; - const userPath = options && 'path' in options ? options.path : '/'; - const path = userPath.startsWith('/') ? userPath : `/${userPath}`; + const routePath = (() => { + if (options && hasOwnProp(options, 'path') && atomix.valueIs.validString(options.path)) { + return options.path?.startsWith('/') ? options.path : `/${options.path}`; + } else { + return '/'; + } + })() const dotfiles = options && 'dotfiles' in options ? options.dotfiles : 'ignore'; - const route = new StaticRoute(root, { path, subDomain, caseSensitive, dotfiles }); + const route = new StaticRoute(root, { path: routePath, subDomain, caseSensitive, dotfiles }); if (this.#_server instanceof HyperCloudServer) { this.#_server._routesManager.add(route); } else { diff --git a/src/services/routes/assets/staticRoute.ts b/src/services/routes/assets/staticRoute.ts index d2d414d..94ed808 100644 --- a/src/services/routes/assets/staticRoute.ts +++ b/src/services/routes/assets/staticRoute.ts @@ -1,9 +1,14 @@ -import { StaticRouteOptions, HyperCloudRequestHandler } from '../../../docs/docs'; -import helpers from '../../../utils/helpers'; +import atomix from "@nasriya/atomix"; +import mimex from "@nasriya/mimex"; +import overwatch from "@nasriya/overwatch"; +import routeCache from "../../cache/routeCache"; +import type { HyperCloudRequestHandler, MimeType, StaticRouteOptions } from "../../../docs/docs"; import fs from 'fs'; import path from 'path'; +const CACHE_SCOPE = 'hypercloud_static_routes' as const; + class StaticRoute { readonly #_root: string; readonly #_configs = { @@ -13,7 +18,6 @@ class StaticRoute { handler: null as unknown as HyperCloudRequestHandler, dotfiles: 'ignore' as 'allow' | 'ignore' | 'deny', path: [] as string[], - memoryCache: true } readonly #_utils = Object.freeze({ @@ -44,99 +48,204 @@ class StaticRoute { if (typeof options.caseSensitive !== 'boolean') { throw new TypeError(`The Route's caseSensitive option is expecting a boolean value, but instead got ${typeof options.caseSensitive}`) } this.#_configs.caseSensitive = options.caseSensitive; } + }, + route: async () => { + await this.#_utils.cache.route(); + await overwatch.watchFolder(this.#_root, { + onRemove: async (event) => { + try { + const record = routeCache.files.inspect({ filePath: event.path, scope: CACHE_SCOPE, caseSensitive: this.#_configs.caseSensitive }); + if (!record) { return } + + await routeCache.files.remove({ + filePath: event.path, + scope: CACHE_SCOPE, + caseSensitive: this.#_configs.caseSensitive + }) + } catch (error) { + console.error(`Failed to remove ${event.path} from cache:`, error); + } + }, + onAdd: async (event) => { + try { + await this.#_utils.cache.createRecord(event.path); + } catch (error) { + console.error(`Failed to add ${event.path} to cache:`, error); + } + } + }); + } + }, + cache: { + createRecord: async (filePath: string) => { + const fileName = path.basename(filePath); + if (fileName.startsWith('.') && this.#_configs.dotfiles !== 'allow') { + return; + } + + return routeCache.files.set(filePath, { + scope: CACHE_SCOPE, + ttl: 1_000 * 60 * 60 // 1 hour + }); + }, + path: (dir: string, setPromises: Promise[]) => { + const content = fs.readdirSync(dir, { withFileTypes: true }); + for (const item of content) { + const contentPath = path.join(dir, item.name); + if (item.isDirectory()) { + this.#_utils.cache.path(contentPath, setPromises); + } else { + const createPromise = this.#_utils.cache.createRecord(contentPath) + setPromises.push(createPromise); + } + } + }, + route: async () => { + const stats = fs.statSync(this.#_root); + const promises: Promise[] = []; + + if (stats.isDirectory()) { + this.#_utils.cache.path(this.#_root, promises); + } else { + const createPromise = this.#_utils.cache.createRecord(this.#_root); + promises.push(createPromise); + } + + await Promise.all(promises); } + }, + getFileMime: (filePath: string) => { + const ext = path.extname(filePath); + const mimes = mimex.getMimes(ext); + return mimes ? mimes[0] : 'text/plain'; + }, + parseFile: (_reqPath: string[]) => { + // Remove the initial path (the virtual path) and keep the root path + const reqPath = _reqPath.slice(this.#_configs.path.length, _reqPath.length).join(path.sep); + const filePath = path.join(this.#_root, reqPath); + + // Prevent path traversal attacks + if (!atomix.path.isSubPath(filePath, this.#_root)) { + const error = new Error(`Path traversal attack detected on path: ${filePath}`); + error.name = 'PathTraversalError'; + throw error; + } + + const fileName = path.basename(filePath); + const mimeType = this.#_utils.getFileMime(filePath) as MimeType; + + return { path: filePath, name: fileName, mimeType } } }) - constructor(root: string, options: StaticRouteOptions) { - const validity = helpers.checkPathAccessibility(root); - if (validity.valid !== true) { - const errors = validity.errors; - if (errors.notString) { throw new Error(`The root directory should be a string value, instead got ${typeof root}`) } - if (errors.doesntExist) { throw new Error(`The provided root directory (${root}) doesn't exist.`) } - if (errors.notAccessible) { throw new Error(`Unable to access (${root}): read permission denied.`) } - } + readonly #_handlers = { + cacheHandler: (async (request, response, next) => { + try { + if (request.path.length < this.#_configs.path.length) { + return response.pages.serverError({ + error: new Error(`Request path is shorter than route prefix. Possible framework route-matching bug.`) + }); + } - this.#_root = root; - this.#_utils.initialize.dotfiles(options); - this.#_utils.initialize.path(options); - this.#_utils.initialize.subDomain(options); - this.#_utils.initialize.caseSensitive(options); + // Parse the file from the request + const reqFile = this.#_utils.parseFile(request.path); - this.#_configs.handler = (request, response, next) => { - try { - if (request.path.length < this.#_configs.path.length) { return response.status(500).end({ data: `Internal server error (500).\n\nIf you're a visitor please wait a few minutes.` }) } - // Remove the initial path (the virtual path) and keep the root path - const reqPath = request.path.slice(this.#_configs.path.length, request.path.length); + // Check the file against the policy + if (reqFile.name.startsWith('.')) { + if (this.#_configs.dotfiles === 'ignore') { return next() } + if (this.#_configs.dotfiles === 'deny') { return response.pages.forbidden() } + } - for (let i = 0; i < reqPath.length; i++) { - const pathSegment = reqPath[i]; - const isLast = i + 1 >= reqPath.length; + // Check if the file exists + const fileRecord = routeCache.files.inspect({ + filePath: reqFile.path, + scope: CACHE_SCOPE, + caseSensitive: this.#_configs.caseSensitive + }); - if (pathSegment.startsWith('.')) { - if (this.#_configs.dotfiles === 'ignore') { return next() } - if (this.#_configs.dotfiles === 'deny') { return response.pages.unauthorized() } - } + if (!fileRecord) { return next(); } - if (!isLast) { continue } + // Define headers values + const modifiedDate = new Date(fileRecord.file.stats.mtime); + const eTag = fileRecord.file.eTag; - const copy = [...reqPath]; // Create a copy of the request path array - copy.pop(); // Removes the last item (resource name) from the copy array + // Check for conditional headers + const ifNoneMatch = request.headers['if-none-match']; + const ifModifiedSince = request.headers['if-modified-since']; - // Resolve the folder path from the root directory and the request path - const folder = path.resolve(path.join(this.#_root, ...copy)); - // Check folder path validity - const validity = helpers.checkPathAccessibility(folder); - if (validity.valid !== true) { return next() } + const isNotModified = (() => { + if (!ifModifiedSince && !ifNoneMatch) { return false } - // Check if the path is an actual directory - const folderStats = fs.statSync(folder); - if (!folderStats.isDirectory()) { return next() } + if (ifNoneMatch) { + // Handle multiple ETags in header (comma-separated) + const clientEtags = ifNoneMatch.split(',').map(tag => tag.trim()); - const filename = pathSegment; - // Read the content of the folder - const content = fs.readdirSync(folder, { withFileTypes: true }); + // Normalize: remove weak prefix and quotes + const normalized = clientEtags.map(tag => + tag.replace(/^W\//, '').replace(/(^"|"$)/g, '') + ); - const file = content.find(i => { - if (this.#_configs.caseSensitive) { - if (i.name === filename) { return true } - } else { - if (i.name.toLowerCase() === filename.toLowerCase()) { return true } + // Check if any match your stored eTag + for (const clientETAG of normalized) { + if (clientETAG === '*' || clientETAG === eTag) { + return true + } } + } - return false - }) - - if (!file || !file.isFile()) { return next() } + if (ifModifiedSince) { + // Validate modification date + const clientDate = new Date(ifModifiedSince); + const isDateValid = clientDate instanceof Date && !isNaN(clientDate.getTime()); - // Check the eTag value if it does exist - const eTagsPath = path.join(folder, 'eTags.json'); - const eTagValidity = helpers.checkPathAccessibility(eTagsPath); - if (eTagValidity.valid) { - const eTags = JSON.parse(fs.readFileSync(eTagsPath, { encoding: 'utf-8' })) - if (helpers.is.realObject(eTags)) { - if (file.name in eTags) { response.setHeader('etag', eTags[file.name]) } - } + return isDateValid && clientDate >= modifiedDate; } - const filePath = path.join(folder, file.name); - return response.sendFile(filePath, { - lastModified: true, - acceptRanges: true, - cacheControl: true, - maxAge: '3 days' - }) + return false; + })(); + + if (isNotModified) { + return response.status(304).end(); } - next(); + response.setHeader('etag', `W/"${eTag}"`); + response.setHeader('last-modified', modifiedDate.toUTCString()); + + const readResponse = await routeCache.files.read({ + key: fileRecord.key, + scope: CACHE_SCOPE, + caseSensitive: this.#_configs.caseSensitive + }); + + if (!readResponse) { return next(); } + + response.setHeader('Cachify-Status', readResponse.status) + response.send(readResponse.content, reqFile.mimeType); } catch (error) { + if (error instanceof Error && error.name === 'PathTraversalError') { + response.pages.forbidden(); + return; + } + console.error(error); - response.status(500).json({ type: 'server_error', code: 500, href: request.href, message: "An internal server error occurred." }) + response.pages.serverError({ error: error as Error }); } - } + }) as HyperCloudRequestHandler + } + + constructor(root: string, options: StaticRouteOptions) { + atomix.fs.canAccessSync(root, { permissions: 'Read', throwError: true }); + + this.#_root = root; + this.#_utils.initialize.dotfiles(options); + this.#_utils.initialize.path(options); + this.#_utils.initialize.subDomain(options); + this.#_utils.initialize.caseSensitive(options); + + this.#_configs.handler = this.#_handlers.cacheHandler; + void this.#_utils.initialize.route().catch(console.error); } - get subDomain(): '*' | string { return this.#_configs.subDomain } get caseSensitive() { return this.#_configs.caseSensitive } get method() { return this.#_configs.method } diff --git a/src/services/uploads/assets/handler.ts b/src/services/uploads/assets/handler.ts index 5170012..8eddbe5 100644 --- a/src/services/uploads/assets/handler.ts +++ b/src/services/uploads/assets/handler.ts @@ -7,8 +7,7 @@ import RequestBody from "../../handler/assets/requestBody"; import fs from "fs"; import helpers from "../../../utils/helpers"; import path from "path"; - -const mimes: MimeType[] = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../data/mimes.json'), { encoding: 'utf8' })); +import mimex from "@nasriya/mimex"; class UploadHandler { #_currentFile: UploadedMemoryFile | UploadedStorageFile | undefined; @@ -111,7 +110,7 @@ class UploadHandler { throw new Error(`The header is invalid`) } - if (!mimes.includes(details.mime)) { + if (!mimex.isMime(details.mime)) { throw new Error(`The request mime type is not supported: ${details.mime}`) } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index eff5989..ba5d899 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,17 +1,10 @@ import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; -import { DeepReadonly, MimeType, RandomOptions } from '../docs/docs'; - -const _dirname = __dirname; +import currencies from '../data/currencies'; +import { Currency, DeepReadonly, MimeType, RandomOptions } from '../docs/docs'; class Helpers { - #_currencies: string[] = []; - - constructor() { - this.#_currencies = this.loadJSON(path.resolve(_dirname, '../data/currencies.json')) as string[]; - } - /** * Load a `JSON` file * @param filePath The absolute path of the `JSON` file @@ -124,7 +117,7 @@ class Helpers { currency: (currency: string): boolean => { if (typeof currency === 'string') { currency = currency.toUpperCase(); - return this.#_currencies.includes(currency); + return currencies.includes(currency as Currency); } else { return false; }