diff --git a/Dockerfile b/Dockerfile index 734bd73d35..897db60a19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,49 @@ -# build stage -FROM node:lts-alpine AS build-stage -# Set environment variables for non-interactive npm installs -ENV NPM_CONFIG_LOGLEVEL warn -ENV CI true +# ── Stage 1: Build the Vue frontend ────────────────────────────────────────── +FROM node:20-alpine AS build-stage + +ENV NPM_CONFIG_LOGLEVEL=warn +ENV CI=true + WORKDIR /app + +RUN npm install -g pnpm@9.11.0 + COPY package.json pnpm-lock.yaml ./ -RUN npm install -g pnpm && pnpm i --frozen-lockfile +RUN pnpm install --frozen-lockfile + COPY . . -RUN pnpm build +RUN NODE_OPTIONS=--max_old_space_size=4096 pnpm exec vite build + +# ── Stage 2: Production image (nginx + API server) ─────────────────────────── +FROM node:20-alpine AS production + +WORKDIR /app + +# Install nginx alongside Node.js +RUN apk add --no-cache nginx -# production stage -FROM nginx:stable-alpine AS production-stage +RUN npm install -g pnpm@9.11.0 + +# Copy package manifests and tsconfig for server +COPY package.json pnpm-lock.yaml tsconfig.server.json ./ + +# Install all deps (tsx is a devDep needed at runtime) +RUN pnpm install --frozen-lockfile + +# Copy server source and the src/ files the server imports +COPY server/ ./server/ +COPY src/ ./src/ + +# Copy built Vue frontend from build stage COPY --from=build-stage /app/dist /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy nginx config +COPY nginx.conf /etc/nginx/http.d/default.conf + +# Expose HTTP port EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] + +ENV API_PORT=3001 + +# Start nginx + API server +CMD sh -c "nginx && pnpm api:start" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..3dff688eb6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + it-tools: + build: + context: . + dockerfile: Dockerfile + ports: + - "80:80" + restart: unless-stopped + environment: + - API_PORT=3001 diff --git a/manual-it-tools-api.md b/manual-it-tools-api.md new file mode 100644 index 0000000000..5cbbfeca0d --- /dev/null +++ b/manual-it-tools-api.md @@ -0,0 +1,1083 @@ +# IT-Tools REST API Manual + +## Overview + +The IT-Tools REST API exposes all 86 developer utilities from the it-tools GUI as HTTP endpoints. It runs as a standalone Node.js server using the Hono framework. + +--- + +## Starting the API Server + +### Prerequisites + +- Node.js 18+ +- pnpm installed (`npm install -g pnpm`) + +### Install dependencies + +```bash +pnpm install +``` + +### Start the server + +**Development mode (with auto-reload):** +```bash +pnpm api:dev +``` + +**Production mode:** +```bash +pnpm api:start +``` + +The server starts on **http://localhost:3000** by default. + +To use a custom port: +```bash +API_PORT=8080 pnpm api:start +``` + +### Verify it's running + +```bash +curl http://localhost:3000/api +``` + +This returns a JSON object listing all available endpoints. + +--- + +## API Reference + +All endpoints accept and return JSON. POST requests require `Content-Type: application/json`. + +- Success responses: `{ "result": ... }` +- Error responses: `{ "error": "message" }` with an appropriate HTTP status code + +--- + +## Crypto Endpoints (`/api/crypto`) + +### `POST /api/crypto/token` +Generate a random token. + +**Body:** +```json +{ + "length": 64, + "withUppercase": true, + "withLowercase": true, + "withNumbers": true, + "withSymbols": false, + "alphabet": "optional custom alphabet" +} +``` + +**Example:** +```bash +curl -X POST http://localhost:3000/api/crypto/token \ + -H 'Content-Type: application/json' \ + -d '{"length":32}' +``` + +--- + +### `POST /api/crypto/hash` +Hash text using a cryptographic algorithm. + +**Body:** +```json +{ + "text": "hello world", + "algorithm": "SHA256", + "encoding": "Hex" +} +``` + +**Algorithms:** `MD5`, `SHA1`, `SHA224`, `SHA256`, `SHA384`, `SHA512`, `SHA3`, `RIPEMD160` + +**Encodings:** `Hex`, `Base64`, `Latin1`, `Bin` + +--- + +### `POST /api/crypto/hmac` +Generate an HMAC signature. + +**Body:** +```json +{ + "text": "message", + "secret": "my-secret", + "algorithm": "SHA256", + "encoding": "Hex" +} +``` + +**Algorithms:** `MD5`, `SHA1`, `SHA224`, `SHA256`, `SHA384`, `SHA512`, `SHA3`, `RIPEMD160` + +**Encodings:** `Hex`, `Base64` + +--- + +### `POST /api/crypto/bcrypt/hash` +Hash a string using bcrypt. + +**Body:** +```json +{ + "text": "my-password", + "saltRounds": 10 +} +``` + +--- + +### `POST /api/crypto/bcrypt/verify` +Verify a bcrypt hash. + +**Body:** +```json +{ + "text": "my-password", + "hash": "$2a$10$..." +} +``` + +**Returns:** `{ "result": true/false }` + +--- + +### `POST /api/crypto/uuid` +Generate UUIDs. + +**Body:** +```json +{ + "version": "v4", + "count": 5, + "namespace": "optional UUID namespace (for v3/v5)", + "name": "optional name string (for v3/v5)" +} +``` + +**Versions:** `NIL`, `v1`, `v3`, `v4`, `v5` + +--- + +### `POST /api/crypto/ulid` +Generate ULIDs (Universally Unique Lexicographically Sortable Identifiers). + +**Body:** +```json +{ "count": 5 } +``` + +--- + +### `POST /api/crypto/encrypt` +Encrypt text. + +**Body:** +```json +{ + "text": "secret message", + "secret": "passphrase", + "algorithm": "AES" +} +``` + +**Algorithms:** `AES`, `TripleDES`, `Rabbit`, `RC4` + +--- + +### `POST /api/crypto/decrypt` +Decrypt encrypted text. + +**Body:** +```json +{ + "text": "U2FsdGVkX1...", + "secret": "passphrase", + "algorithm": "AES" +} +``` + +--- + +### `POST /api/crypto/rsa/generate` +Generate an RSA key pair. + +**Body:** +```json +{ "bits": 2048 } +``` + +**Returns:** `{ "result": { "publicKey": "-----BEGIN PUBLIC KEY-----...", "privateKey": "..." } }` + +--- + +### `POST /api/crypto/bip39/generate` +Generate a BIP39 mnemonic phrase. + +**Body:** +```json +{ "entropyBits": 128 } +``` + +**Valid entropyBits values:** `128`, `160`, `192`, `224`, `256` (maps to 12, 15, 18, 21, 24 words) + +--- + +### `POST /api/crypto/bip39/to-entropy` +Convert a BIP39 mnemonic back to its entropy hex. + +**Body:** +```json +{ "mnemonic": "word1 word2 word3 ..." } +``` + +--- + +### `POST /api/crypto/otp/secret` +Generate a random OTP secret key (base32 encoded). + +No body required. + +--- + +### `POST /api/crypto/otp/totp/generate` +Generate a TOTP (time-based OTP) code. + +**Body:** +```json +{ + "key": "BASE32SECRET", + "timeStep": 30 +} +``` + +--- + +### `POST /api/crypto/otp/totp/verify` +Verify a TOTP code. + +**Body:** +```json +{ + "key": "BASE32SECRET", + "token": "123456", + "window": 1, + "timeStep": 30 +} +``` + +--- + +### `POST /api/crypto/otp/hotp/generate` +Generate an HOTP (counter-based OTP) code. + +**Body:** +```json +{ + "key": "BASE32SECRET", + "counter": 0 +} +``` + +--- + +### `POST /api/crypto/otp/hotp/verify` +Verify an HOTP code. + +**Body:** +```json +{ + "key": "BASE32SECRET", + "token": "123456", + "counter": 0, + "window": 0 +} +``` + +--- + +### `POST /api/crypto/otp/key-uri` +Build an `otpauth://` URI for use with authenticator apps / QR codes. + +**Body:** +```json +{ + "secret": "BASE32SECRET", + "app": "MyApp", + "account": "user@example.com", + "algorithm": "SHA1", + "digits": 6, + "period": 30 +} +``` + +--- + +### `POST /api/crypto/password-strength` +Analyze password strength and estimate crack time. + +**Body:** +```json +{ "password": "my-password-123" } +``` + +--- + +## Converter Endpoints (`/api/converter`) + +### `POST /api/converter/base64/encode` +Encode text to Base64. + +**Body:** +```json +{ "text": "Hello World", "urlSafe": false } +``` + +--- + +### `POST /api/converter/base64/decode` +Decode Base64 text. + +**Body:** +```json +{ "text": "SGVsbG8gV29ybGQ=", "urlSafe": false } +``` + +Also accepts `data:` URI prefix (e.g., `data:image/png;base64,...`). + +--- + +### `POST /api/converter/case` +Convert text to all case formats. + +**Body:** +```json +{ "text": "hello world foo" } +``` + +**Returns all of:** `lowercase`, `uppercase`, `camelCase`, `capitalCase`, `constantCase`, `dotCase`, `headerCase`, `noCase`, `paramCase`, `pascalCase`, `pathCase`, `sentenceCase`, `snakeCase` + +--- + +### `POST /api/converter/roman-numeral/to-roman` +Convert arabic number to Roman numeral. + +**Body:** +```json +{ "number": 42 } +``` + +--- + +### `POST /api/converter/roman-numeral/to-arabic` +Convert Roman numeral to arabic number. + +**Body:** +```json +{ "roman": "XLII" } +``` + +--- + +### `POST /api/converter/yaml-to-json` +Convert YAML to JSON. + +**Body:** +```json +{ "yaml": "name: foo\nage: 30" } +``` + +--- + +### `POST /api/converter/json-to-yaml` +Convert JSON to YAML. + +**Body:** +```json +{ "json": "{\"name\":\"foo\",\"age\":30}" } +``` + +--- + +### `POST /api/converter/yaml-to-toml` +Convert YAML to TOML. + +**Body:** +```json +{ "yaml": "name: foo\nvalue: 42" } +``` + +--- + +### `POST /api/converter/json-to-toml` +Convert JSON to TOML. + +**Body:** +```json +{ "json": "{\"name\":\"foo\",\"value\":42}" } +``` + +--- + +### `POST /api/converter/toml-to-json` +Convert TOML to JSON. + +**Body:** +```json +{ "toml": "name = \"foo\"\nvalue = 42" } +``` + +--- + +### `POST /api/converter/toml-to-yaml` +Convert TOML to YAML. + +**Body:** +```json +{ "toml": "name = \"foo\"\nvalue = 42" } +``` + +--- + +### `POST /api/converter/xml-to-json` +Convert XML to JSON. + +**Body:** +```json +{ "xml": "test" } +``` + +--- + +### `POST /api/converter/json-to-xml` +Convert JSON to XML. + +**Body:** +```json +{ "json": "{\"root\":{\"name\":\"test\"}}" } +``` + +--- + +### `POST /api/converter/markdown-to-html` +Render Markdown to HTML. + +**Body:** +```json +{ "markdown": "# Hello\nThis is **bold**." } +``` + +--- + +### `POST /api/converter/color` +Convert a color value between formats. + +**Body:** +```json +{ "color": "#ff6600" } +``` + +Accepts: hex (`#ff6600`), rgb (`rgb(255,102,0)`), hsl (`hsl(24,100%,50%)`), CSS color names, etc. + +**Returns:** `hex`, `rgb`, `hsl`, `hsv` + +--- + +### `POST /api/converter/text-to-binary` +Convert text to ASCII binary representation. + +**Body:** +```json +{ "text": "Hello" } +``` + +--- + +### `POST /api/converter/binary-to-text` +Convert ASCII binary back to text. + +**Body:** +```json +{ "binary": "01001000 01100101 01101100 01101100 01101111" } +``` + +--- + +### `POST /api/converter/text-to-unicode` +Convert text to Unicode escape sequences. + +**Body:** +```json +{ "text": "Hello 🌍" } +``` + +--- + +### `POST /api/converter/unicode-to-text` +Convert Unicode escape sequences back to text. + +**Body:** +```json +{ "unicode": "\\u0048\\u0065\\u006C\\u006C\\u006F" } +``` + +--- + +### `POST /api/converter/text-to-nato` +Spell out text using the NATO phonetic alphabet. + +**Body:** +```json +{ "text": "SOS" } +``` + +--- + +### `POST /api/converter/integer-base` +Convert an integer between numeric bases. + +**Body:** +```json +{ + "value": "255", + "fromBase": 10, + "toBase": 16 +} +``` + +--- + +### `POST /api/converter/temperature` +Convert a temperature value between all scales. + +**Body:** +```json +{ "value": 100, "from": "celsius" } +``` + +**Scales:** `celsius`, `fahrenheit`, `kelvin`, `rankine`, `delisle`, `newton`, `reaumur`, `romer` + +**Returns all scales** in the result. + +--- + +### `POST /api/converter/slugify` +Convert text to a URL-friendly slug. + +**Body:** +```json +{ "text": "Hello World! This is a test." } +``` + +--- + +## Web Endpoints (`/api/web`) + +### `POST /api/web/url/encode` +URL-encode a string. + +**Body:** +```json +{ "text": "hello world & foo=bar" } +``` + +--- + +### `POST /api/web/url/decode` +URL-decode a string. + +**Body:** +```json +{ "text": "hello%20world%20%26%20foo%3Dbar" } +``` + +--- + +### `POST /api/web/url/parse` +Parse a URL into its components. + +**Body:** +```json +{ "url": "https://user:pass@example.com:8080/path?foo=bar#hash" } +``` + +**Returns:** `protocol`, `username`, `password`, `hostname`, `port`, `pathname`, `search`, `params`, `hash`, `href`, `origin` + +--- + +### `POST /api/web/basic-auth/generate` +Generate a Basic Auth header. + +**Body:** +```json +{ "username": "admin", "password": "secret" } +``` + +**Returns:** `{ "header": "Authorization: Basic ...", "token": "..." }` + +--- + +### `POST /api/web/jwt/parse` +Decode and parse a JWT token (without verification). + +**Body:** +```json +{ "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } +``` + +--- + +### `POST /api/web/html-entities/encode` +Encode HTML special characters. + +**Body:** +```json +{ "text": "

Hello & World

" } +``` + +--- + +### `POST /api/web/html-entities/decode` +Decode HTML entities. + +**Body:** +```json +{ "text": "<h1>Hello & World</h1>" } +``` + +--- + +### `POST /api/web/safelink/decode` +Decode a Microsoft Safelinks-wrapped URL. + +**Body:** +```json +{ "url": "https://nam12.safelinks.protection.outlook.com/?url=https%3A%2F%2Fexample.com&data=..." } +``` + +--- + +### `POST /api/web/user-agent/parse` +Parse a User-Agent string. + +**Body:** +```json +{ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } +``` + +**Returns:** `ua`, `browser`, `engine`, `os`, `device`, `cpu` + +--- + +### `POST /api/web/mime-types/lookup` +Look up a MIME type by extension or vice versa. + +**Body:** +```json +{ "query": "json" } +``` + +Also accepts MIME types as input (e.g., `"application/json"`). + +--- + +### `GET /api/web/http-status-codes` +List all HTTP status codes. + +No body. Returns array of `{ code, message, category }`. + +--- + +### `POST /api/web/http-status-codes/lookup` +Look up a specific HTTP status code. + +**Body:** +```json +{ "code": 404 } +``` + +--- + +## Development Endpoints (`/api/development`) + +### `POST /api/development/json/prettify` +Pretty-print JSON. + +**Body:** +```json +{ "json": "{\"a\":1,\"b\":2}", "indent": 2 } +``` + +--- + +### `POST /api/development/json/minify` +Minify JSON. + +**Body:** +```json +{ "json": "{\n \"a\": 1\n}" } +``` + +--- + +### `POST /api/development/json/to-csv` +Convert a JSON array to CSV. + +**Body:** +```json +{ "json": "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]" } +``` + +--- + +### `POST /api/development/json/diff` +Compare two JSON values (pretty-printed for comparison). + +**Body:** +```json +{ "left": "{\"a\":1}", "right": "{\"a\":2}" } +``` + +**Returns:** `{ "left": "...", "right": "...", "identical": false }` + +--- + +### `POST /api/development/sql/prettify` +Format and prettify SQL. + +**Body:** +```json +{ + "sql": "SELECT id,name FROM users WHERE active=1", + "language": "sql", + "keywordCase": "upper" +} +``` + +**Languages:** `sql`, `mysql`, `postgresql`, `sqlite`, `tsql` + +**keywordCase:** `upper`, `lower`, `preserve` + +--- + +### `POST /api/development/chmod/calculate` +Calculate chmod permissions. + +**Body:** +```json +{ + "owner": { "read": true, "write": true, "execute": false }, + "group": { "read": true, "write": false, "execute": false }, + "public": { "read": true, "write": false, "execute": false } +} +``` + +Note: `others` is accepted as an alias for `public`. + +**Returns:** `{ "octal": "644", "symbolic": "rw-r--r--", "command": "chmod 644" }` + +--- + +### `POST /api/development/docker/to-compose` +Convert a `docker run` command to Docker Compose YAML. + +**Body:** +```json +{ "dockerRun": "docker run -d -p 8080:80 --name myapp nginx" } +``` + +Note: `command` is accepted as an alias for `dockerRun`. + +--- + +### `POST /api/development/xml/format` +Format/prettify XML. + +**Body:** +```json +{ "xml": "text", "indentSize": 2 } +``` + +--- + +### `POST /api/development/yaml/format` +Validate and re-format YAML. + +**Body:** +```json +{ "yaml": "name: foo\nage: 30" } +``` + +--- + +### `POST /api/development/email/normalize` +Normalize email addresses (removes dots, tags, etc.). + +**Body:** +```json +{ "emails": "User.Name+tag@gmail.com" } +``` + +Also accepts newline-separated list of emails or an array. + +--- + +### `POST /api/development/regex/test` +Test a regular expression against a string. + +**Body:** +```json +{ + "pattern": "\\d+", + "flags": "g", + "text": "abc 123 def 456" +} +``` + +**Returns:** `{ "matches": [{ "match": "123", "index": 4 }, ...], "count": 2, "isValid": true }` + +--- + +### `GET /api/development/port/random` +Generate a random available port number (1024–65535). + +No body. + +--- + +## Network Endpoints (`/api/network`) + +### `POST /api/network/ipv4/subnet` +Calculate IPv4 subnet information from a CIDR. + +**Body:** +```json +{ "cidr": "192.168.1.0/24" } +``` + +**Returns:** `networkAddress`, `broadcastAddress`, `subnetMask`, `wildcardMask`, `firstHost`, `lastHost`, `totalHosts`, `usableHosts`, `ipClass`, `cidr` + +--- + +### `POST /api/network/ipv4/convert` +Convert an IPv4 address to multiple formats. + +**Body:** +```json +{ "ip": "192.168.1.1" } +``` + +**Returns:** `dotDecimal`, `decimal`, `hex`, `binary`, `ipv6` + +--- + +### `POST /api/network/ipv4/range` +Calculate the CIDR block for a range of IPs. + +**Body:** +```json +{ "startIp": "192.168.1.0", "endIp": "192.168.1.255" } +``` + +--- + +### `POST /api/network/mac/generate` +Generate random MAC addresses. + +**Body:** +```json +{ + "count": 3, + "prefix": "00:50:56", + "separator": ":" +} +``` + +--- + +### `POST /api/network/mac/lookup` +Look up the vendor for a MAC address (OUI lookup). + +**Body:** +```json +{ "mac": "00:50:56:AB:CD:EF" } +``` + +**Returns:** `{ "mac": "...", "vendor": "VMware, Inc.", "found": true }` + +--- + +### `POST /api/network/ipv6/ula` +Generate an IPv6 Unique Local Address (ULA). + +**Body:** +```json +{ "mac": "00:50:56:AB:CD:EF" } +``` + +**Returns:** `ula`, `firstRoutableBlock`, `lastRoutableBlock` + +--- + +## Math Endpoints (`/api/math`) + +### `POST /api/math/evaluate` +Evaluate a mathematical expression. + +**Body:** +```json +{ "expression": "2 + 3 * 4" } +``` + +Supports arithmetic, trigonometry, units, matrices, and more (powered by mathjs). + +--- + +### `POST /api/math/percentage` +Calculate percentages. + +**Body:** +```json +{ "x": 25, "y": 200 } +``` + +Optionally specify `type`: +- `"percent-of"` → What is x% of y? +- `"is-what-percent"` → x is what % of y? +- `"percent-change"` → % change from x to y + +Without `type`, all three are returned. + +--- + +## Text Endpoints (`/api/text`) + +### `POST /api/text/lorem-ipsum` +Generate Lorem Ipsum placeholder text. + +**Body:** +```json +{ + "paragraphCount": 2, + "sentencePerParagraph": 3, + "wordCount": 10, + "startWithLoremIpsum": true, + "asHTML": false +} +``` + +--- + +### `POST /api/text/statistics` +Analyze text statistics. + +**Body:** +```json +{ "text": "Hello world. This is a test.\nSecond line." } +``` + +**Returns:** `characters`, `charactersNoSpaces`, `words`, `sentences`, `lines`, `paragraphs`, `bytes` + +--- + +### `POST /api/text/numeronym` +Generate a numeronym for a word or phrase (e.g., `i18n` for `internationalization`). + +**Body:** +```json +{ "text": "internationalization" } +``` + +--- + +### `POST /api/text/obfuscate` +Obfuscate a string (e.g., for displaying API keys). + +**Body:** +```json +{ + "text": "my-secret-api-key", + "keepFirst": 4, + "keepLast": 0, + "keepSpace": true, + "replacementChar": "*" +} +``` + +--- + +### `POST /api/text/diff` +Compare two texts line-by-line. + +**Body:** +```json +{ + "left": "foo\nbar\nbaz", + "right": "foo\nbaz\nqux" +} +``` + +**Returns:** `{ "identical": false, "addedLines": 1, "removedLines": 1 }` + +--- + +## Data Endpoints (`/api/data`) + +### `POST /api/data/iban/validate` +Validate and parse an IBAN. + +**Body:** +```json +{ "iban": "GB82WEST12345698765432" } +``` + +**Returns:** `isValid`, `errors`, `electronicFormat`, `friendlyFormat`, `bban`, `countryCode` + +--- + +### `POST /api/data/phone/parse` +Parse and validate a phone number. + +**Body:** +```json +{ + "phone": "+14155552671", + "defaultCountry": "US" +} +``` + +**Returns:** `isValid`, `isPossible`, `country`, `countryCallingCode`, `nationalNumber`, `number`, `e164`, `international`, `national`, `type` + +--- + +### `GET /api/data/phone/countries` +List all supported countries with calling codes. + +No body. Returns array of `{ code, callingCode }`. + +--- + +## Error Handling + +All endpoints follow a consistent error response format: + +```json +{ "error": "Description of what went wrong" } +``` + +Common HTTP status codes: +- `400` — Invalid input or unsupported value +- `404` — Not found (e.g., unknown status code) +- `500` — Internal server error + +--- + +## CORS + +The API has CORS enabled and accepts requests from any origin. It is suitable for use as a local backend or in development environments. diff --git a/nginx.conf b/nginx.conf index 1a30e15e43..641edce5d1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,7 +4,17 @@ server { root /usr/share/nginx/html; index index.html; + # Proxy all /api/* requests to the Hono API server + location /api { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Serve Vue frontend — fall back to index.html for SPA routing location / { try_files $uri $uri/ /index.html; } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 5738b6325a..793a1f5b42 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,12 @@ "lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", "script:create:tool": "node scripts/create-tool.mjs", "script:create:ui": "hygen generator ui-component", - "release": "node ./scripts/release.mjs" + "release": "node ./scripts/release.mjs", + "api:dev": "tsx --tsconfig tsconfig.server.json server/index.ts", + "api:start": "tsx --tsconfig tsconfig.server.json server/index.ts" }, "dependencies": { + "@hono/node-server": "^2.0.5", "@it-tools/bip39": "^0.0.4", "@it-tools/oggen": "^1.3.0", "@regexper/render": "^1.0.0", @@ -68,6 +71,7 @@ "figue": "^1.2.0", "fuse.js": "^6.6.2", "highlight.js": "^11.7.0", + "hono": "^4.12.26", "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", "js-base64": "^3.7.6", @@ -89,6 +93,7 @@ "plausible-tracker": "^0.3.8", "qrcode": "^1.5.1", "randexp": "^0.5.3", + "smol-toml": "^1.6.1", "sql-formatter": "^13.0.0", "ua-parser-js": "^1.0.35", "ulid": "^2.3.0", @@ -137,6 +142,7 @@ "jsdom": "^22.0.0", "less": "^4.1.3", "prettier": "^3.0.0", + "tsx": "^4.22.4", "typescript": "~5.2.0", "unocss": "^0.65.1", "unocss-preset-scrollbar": "^0.2.1", @@ -149,5 +155,10 @@ "vitest": "^0.34.0", "workbox-window": "^7.0.0", "zx": "^7.2.1" + }, + "pnpm": { + "overrides": { + "@vueuse/shared": "10.3.0" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d11389323..60af8daa31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hono/node-server': + specifier: ^2.0.5 + version: 2.0.5(hono@4.12.26) '@it-tools/bip39': specifier: ^0.0.4 version: 0.0.4 @@ -49,7 +52,7 @@ importers: version: 10.3.0(vue@3.3.4) '@vueuse/head': specifier: ^1.0.0 - version: 1.0.0(typescript@5.2.2)(vue@3.3.4) + version: 1.0.0(vue@3.3.4) '@vueuse/router': specifier: ^10.0.0 version: 10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4) @@ -101,6 +104,9 @@ importers: highlight.js: specifier: ^11.7.0 version: 11.7.0 + hono: + specifier: ^4.12.26 + version: 4.12.26 iarna-toml-esm: specifier: ^3.0.5 version: 3.0.5 @@ -164,6 +170,9 @@ importers: randexp: specifier: ^0.5.3 version: 0.5.3 + smol-toml: + specifier: ^1.6.1 + version: 1.6.1 sql-formatter: specifier: ^13.0.0 version: 13.0.0 @@ -303,6 +312,9 @@ importers: prettier: specifier: ^3.0.0 version: 3.0.0 + tsx: + specifier: ^4.22.4 + version: 4.22.4 typescript: specifier: ~5.2.0 version: 5.2.2 @@ -1173,6 +1185,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -1185,6 +1203,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -1197,6 +1221,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -1209,6 +1239,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -1221,6 +1257,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -1233,6 +1275,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -1245,6 +1293,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -1257,6 +1311,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -1269,6 +1329,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -1281,6 +1347,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -1293,6 +1365,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -1305,6 +1383,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -1317,6 +1401,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -1329,6 +1419,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -1341,6 +1437,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -1353,6 +1455,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -1365,6 +1473,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -1377,12 +1497,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.23.1': resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -1395,6 +1527,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -1407,6 +1551,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -1419,6 +1569,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -1431,6 +1587,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -1443,6 +1605,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1461,6 +1629,12 @@ packages: resolution: {integrity: sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@hono/node-server@2.0.5': + resolution: {integrity: sha512-yQFvDmyDo3y6rEOJZDUYPJ49DIKTPpIk4kGvm40xx4Ejne0Pu9a1+exxPN+C1UppWK/WGZX9F++/Xs231tE86g==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@humanwhocodes/config-array@0.11.10': resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -2303,9 +2477,6 @@ packages: '@vue/compiler-core@3.3.7': resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==} - '@vue/compiler-core@3.5.13': - resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} - '@vue/compiler-dom@3.2.47': resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} @@ -2315,18 +2486,12 @@ packages: '@vue/compiler-dom@3.3.7': resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==} - '@vue/compiler-dom@3.5.13': - resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} - '@vue/compiler-sfc@3.2.47': resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} '@vue/compiler-sfc@3.3.4': resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} - '@vue/compiler-sfc@3.5.13': - resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} - '@vue/compiler-ssr@3.2.47': resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} @@ -2336,9 +2501,6 @@ packages: '@vue/compiler-ssr@3.3.7': resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==} - '@vue/compiler-ssr@3.5.13': - resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} - '@vue/devtools-api@6.5.0': resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} @@ -2359,21 +2521,12 @@ packages: '@vue/reactivity@3.3.4': resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} - '@vue/reactivity@3.5.13': - resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} - '@vue/runtime-core@3.3.4': resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} - '@vue/runtime-core@3.5.13': - resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} - '@vue/runtime-dom@3.3.4': resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} - '@vue/runtime-dom@3.5.13': - resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} - '@vue/server-renderer@3.3.4': resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} peerDependencies: @@ -2384,11 +2537,6 @@ packages: peerDependencies: vue: 3.3.7 - '@vue/server-renderer@3.5.13': - resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} - peerDependencies: - vue: 3.5.13 - '@vue/shared@3.2.47': resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} @@ -2398,9 +2546,6 @@ packages: '@vue/shared@3.3.7': resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==} - '@vue/shared@3.5.13': - resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} - '@vue/test-utils@2.3.2': resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==} peerDependencies: @@ -2434,8 +2579,10 @@ packages: '@vueuse/shared@10.3.0': resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} - '@vueuse/shared@12.0.0': - resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} + '@vueuse/shared@14.3.0': + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + peerDependencies: + vue: ^3.5.0 '@zhead/schema@1.0.0-beta.13': resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==} @@ -2896,9 +3043,6 @@ packages: csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} @@ -3173,6 +3317,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -3246,6 +3395,7 @@ packages: eslint-plugin-i@2.28.0-2: resolution: {integrity: sha512-z48kG4qmE4TmiLcxbmvxMT5ycwvPkXaWW0XpU1L768uZaTbiDbxsHMEdV24JHlOR1xDsPpKW39BfP/pRdYIwFA==} engines: {node: '>=12'} + deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead peerDependencies: eslint: ^7.2.0 || ^8 @@ -3649,10 +3799,6 @@ packages: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} - hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3675,6 +3821,10 @@ packages: resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} engines: {node: '>=12.0.0'} + hono@4.12.26: + resolution: {integrity: sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -3817,9 +3967,6 @@ packages: is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-core-module@2.16.0: resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} engines: {node: '>= 0.4'} @@ -4868,10 +5015,6 @@ packages: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - resolve@1.22.9: resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} hasBin: true @@ -5044,6 +5187,10 @@ packages: smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + snake-case@2.1.0: resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} @@ -5071,6 +5218,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -5293,8 +5441,8 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsx@4.19.2: - resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} engines: {node: '>=18.0.0'} hasBin: true @@ -5701,14 +5849,6 @@ packages: vue@3.3.4: resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} - vue@3.5.13: - resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - vuedraggable@4.1.0: resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} peerDependencies: @@ -6097,7 +6237,7 @@ snapshots: '@babel/traverse': 7.22.10 '@babel/types': 7.22.10 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6117,7 +6257,7 @@ snapshots: '@babel/traverse': 7.23.2 '@babel/types': 7.23.0 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7000,7 +7140,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.10 '@babel/types': 7.22.10 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7015,7 +7155,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.5 '@babel/parser': 7.22.5 '@babel/types': 7.22.5 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7030,7 +7170,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.0 '@babel/types': 7.23.0 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7083,141 +7223,219 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true '@esbuild/android-arm64@0.23.1': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.18.20': optional: true '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.18.20': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true '@esbuild/linux-ia32@0.23.1': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true '@esbuild/linux-s390x@0.23.1': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true '@esbuild/linux-x64@0.23.1': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true '@esbuild/netbsd-x64@0.23.1': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.23.1': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true '@esbuild/win32-x64@0.23.1': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.47.0)': dependencies: eslint: 8.47.0 @@ -7228,7 +7446,7 @@ snapshots: '@eslint/eslintrc@2.1.2': dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.4.0 espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -7241,10 +7459,14 @@ snapshots: '@eslint/js@8.47.0': {} + '@hono/node-server@2.0.5(hono@4.12.26)': + dependencies: + hono: 4.12.26 + '@humanwhocodes/config-array@0.11.10': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7264,7 +7486,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.6 '@iconify/types': 2.0.0 - debug: 4.3.4 + debug: 4.4.0 kolorist: 1.8.0 local-pkg: 0.4.3 transitivePeerDependencies: @@ -7401,7 +7623,7 @@ snapshots: '@linaria/logger@4.0.0': dependencies: - debug: 4.3.4 + debug: 4.4.0 picocolors: 1.0.0 transitivePeerDependencies: - supports-color @@ -7922,7 +8144,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) '@typescript-eslint/utils': 6.4.1(eslint@8.47.0)(typescript@5.2.2) - debug: 4.3.4 + debug: 4.4.0 eslint: 8.47.0 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: @@ -7940,10 +8162,10 @@ snapshots: dependencies: '@typescript-eslint/types': 5.60.0 '@typescript-eslint/visitor-keys': 5.60.0 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7954,10 +8176,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.4.1 '@typescript-eslint/visitor-keys': 6.4.1 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7968,10 +8190,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.9.1 '@typescript-eslint/visitor-keys': 6.9.1 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7988,7 +8210,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.2.2) eslint: 8.47.0 eslint-scope: 5.1.1 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8002,7 +8224,7 @@ snapshots: '@typescript-eslint/types': 6.4.1 '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) eslint: 8.47.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8016,7 +8238,7 @@ snapshots: '@typescript-eslint/types': 6.9.1 '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) eslint: 8.47.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8049,15 +8271,13 @@ snapshots: dependencies: '@unhead/schema': 0.5.1 - '@unhead/vue@0.5.1(typescript@5.2.2)(vue@3.3.4)': + '@unhead/vue@0.5.1(vue@3.3.4)': dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 12.0.0(typescript@5.2.2) + '@vueuse/shared': 14.3.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 - transitivePeerDependencies: - - typescript '@unocss/astro@0.65.1(rollup@2.79.2)(vite@4.4.9(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0))(vue@3.3.4)': dependencies: @@ -8341,14 +8561,6 @@ snapshots: source-map-js: 1.0.2 optional: true - '@vue/compiler-core@3.5.13': - dependencies: - '@babel/parser': 7.26.3 - '@vue/shared': 3.5.13 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - '@vue/compiler-dom@3.2.47': dependencies: '@vue/compiler-core': 3.2.47 @@ -8365,11 +8577,6 @@ snapshots: '@vue/shared': 3.3.7 optional: true - '@vue/compiler-dom@3.5.13': - dependencies: - '@vue/compiler-core': 3.5.13 - '@vue/shared': 3.5.13 - '@vue/compiler-sfc@3.2.47': dependencies: '@babel/parser': 7.21.4 @@ -8396,18 +8603,6 @@ snapshots: postcss: 8.4.28 source-map-js: 1.0.2 - '@vue/compiler-sfc@3.5.13': - dependencies: - '@babel/parser': 7.26.3 - '@vue/compiler-core': 3.5.13 - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 - estree-walker: 2.0.2 - magic-string: 0.30.15 - postcss: 8.4.49 - source-map-js: 1.2.1 - '@vue/compiler-ssr@3.2.47': dependencies: '@vue/compiler-dom': 3.2.47 @@ -8424,11 +8619,6 @@ snapshots: '@vue/shared': 3.3.7 optional: true - '@vue/compiler-ssr@3.5.13': - dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/shared': 3.5.13 - '@vue/devtools-api@6.5.0': {} '@vue/language-core@1.8.1(typescript@5.2.2)': @@ -8464,33 +8654,17 @@ snapshots: dependencies: '@vue/shared': 3.3.4 - '@vue/reactivity@3.5.13': - dependencies: - '@vue/shared': 3.5.13 - '@vue/runtime-core@3.3.4': dependencies: '@vue/reactivity': 3.3.4 '@vue/shared': 3.3.4 - '@vue/runtime-core@3.5.13': - dependencies: - '@vue/reactivity': 3.5.13 - '@vue/shared': 3.5.13 - '@vue/runtime-dom@3.3.4': dependencies: '@vue/runtime-core': 3.3.4 '@vue/shared': 3.3.4 csstype: 3.1.2 - '@vue/runtime-dom@3.5.13': - dependencies: - '@vue/reactivity': 3.5.13 - '@vue/runtime-core': 3.5.13 - '@vue/shared': 3.5.13 - csstype: 3.1.3 - '@vue/server-renderer@3.3.4(vue@3.3.4)': dependencies: '@vue/compiler-ssr': 3.3.4 @@ -8504,12 +8678,6 @@ snapshots: vue: 3.3.4 optional: true - '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.2.2))': - dependencies: - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 - vue: 3.5.13(typescript@5.2.2) - '@vue/shared@3.2.47': {} '@vue/shared@3.3.4': {} @@ -8517,8 +8685,6 @@ snapshots: '@vue/shared@3.3.7': optional: true - '@vue/shared@3.5.13': {} - '@vue/test-utils@2.3.2(vue@3.3.4)': dependencies: js-beautify: 1.14.6 @@ -8546,14 +8712,12 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/head@1.0.0(typescript@5.2.2)(vue@3.3.4)': + '@vueuse/head@1.0.0(vue@3.3.4)': dependencies: '@unhead/schema': 0.5.1 '@unhead/ssr': 0.5.1 - '@unhead/vue': 0.5.1(typescript@5.2.2)(vue@3.3.4) + '@unhead/vue': 0.5.1(vue@3.3.4) vue: 3.3.4 - transitivePeerDependencies: - - typescript '@vueuse/metadata@10.3.0': {} @@ -8580,11 +8744,9 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/shared@12.0.0(typescript@5.2.2)': + '@vueuse/shared@14.3.0(vue@3.3.4)': dependencies: - vue: 3.5.13(typescript@5.2.2) - transitivePeerDependencies: - - typescript + vue: 3.3.4 '@zhead/schema@1.0.0-beta.13': {} @@ -8608,7 +8770,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -8780,7 +8942,7 @@ snapshots: builtins@5.0.1: dependencies: - semver: 7.5.4 + semver: 7.6.3 bundle-require@5.0.0(esbuild@0.23.1): dependencies: @@ -9084,8 +9246,6 @@ snapshots: csstype@3.1.2: {} - csstype@3.1.3: {} - dash-get@1.0.2: {} data-uri-to-buffer@4.0.1: {} @@ -9415,6 +9575,35 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.1.1: {} escalade@3.2.0: {} @@ -9971,10 +10160,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.0: - dependencies: - function-bind: 1.1.2 - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -9995,6 +10180,8 @@ snapshots: highlight.js@11.9.0: {} + hono@4.12.26: {} + hookable@5.5.3: {} hosted-git-info@2.8.9: {} @@ -10016,14 +10203,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -10084,7 +10271,7 @@ snapshots: jiti: 2.0.0-beta.3 jiti-v1: jiti@1.21.6 pathe: 1.1.2 - tsx: 4.19.2 + tsx: 4.22.4 transitivePeerDependencies: - supports-color @@ -10160,10 +10347,6 @@ snapshots: dependencies: has: 1.0.3 - is-core-module@2.13.1: - dependencies: - hasown: 2.0.0 - is-core-module@2.16.0: dependencies: hasown: 2.0.2 @@ -10753,7 +10936,7 @@ snapshots: normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.8 + resolve: 1.22.9 semver: 5.7.2 validate-npm-package-license: 3.0.4 @@ -11261,19 +11444,13 @@ snapshots: resolve@1.22.2: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 resolve@1.22.4: dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -11453,6 +11630,8 @@ snapshots: smob@1.5.0: {} + smol-toml@1.6.1: {} + snake-case@2.1.0: dependencies: no-case: 2.3.2 @@ -11717,10 +11896,9 @@ snapshots: tslib: 1.14.1 typescript: 5.2.2 - tsx@4.19.2: + tsx@4.22.4: dependencies: - esbuild: 0.23.1 - get-tsconfig: 4.8.1 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 @@ -12018,7 +12196,7 @@ snapshots: vite-node@0.34.0(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.4.0 mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -12125,14 +12303,14 @@ snapshots: vue-eslint-parser@9.3.1(eslint@8.47.0): dependencies: - debug: 4.3.4 + debug: 4.4.0 eslint: 8.47.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.5.0 lodash: 4.17.21 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -12180,16 +12358,6 @@ snapshots: '@vue/server-renderer': 3.3.4(vue@3.3.4) '@vue/shared': 3.3.4 - vue@3.5.13(typescript@5.2.2): - dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-sfc': 3.5.13 - '@vue/runtime-dom': 3.5.13 - '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.2.2)) - '@vue/shared': 3.5.13 - optionalDependencies: - typescript: 5.2.2 - vuedraggable@4.1.0(vue@3.3.4): dependencies: sortablejs: 1.14.0 diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000000..6d8f17da8c --- /dev/null +++ b/server/index.ts @@ -0,0 +1,150 @@ +import { serve } from '@hono/node-server'; +import { serveStatic } from '@hono/node-server/serve-static'; +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import cryptoRoutes from './routes/crypto.js'; +import converterRoutes from './routes/converter.js'; +import webRoutes from './routes/web.js'; +import developmentRoutes from './routes/development.js'; +import networkRoutes from './routes/network.js'; +import mathRoutes from './routes/math.js'; +import textRoutes from './routes/text.js'; +import dataRoutes from './routes/data.js'; + +const app = new Hono(); + +app.use('*', cors()); +app.use('*', logger()); + +// Mount route groups +app.route('/api/crypto', cryptoRoutes); +app.route('/api/converter', converterRoutes); +app.route('/api/web', webRoutes); +app.route('/api/development', developmentRoutes); +app.route('/api/network', networkRoutes); +app.route('/api/math', mathRoutes); +app.route('/api/text', textRoutes); +app.route('/api/data', dataRoutes); + +// API index — list all available endpoints +app.get('/api', (c) => { + return c.json({ + name: 'IT-Tools REST API', + version: '1.0.0', + description: 'REST API exposing all IT-Tools utilities', + endpoints: { + crypto: { + 'POST /api/crypto/token': 'Generate a random token', + 'POST /api/crypto/hash': 'Hash text (MD5, SHA256, etc.)', + 'POST /api/crypto/hmac': 'Generate HMAC (MD5, SHA256, etc.)', + 'POST /api/crypto/bcrypt/hash': 'Bcrypt hash a string', + 'POST /api/crypto/bcrypt/verify': 'Verify a string against a bcrypt hash', + 'POST /api/crypto/uuid': 'Generate UUIDs (v1/v3/v4/v5/NIL)', + 'POST /api/crypto/ulid': 'Generate ULIDs', + 'POST /api/crypto/encrypt': 'Encrypt text (AES/TripleDES/Rabbit/RC4)', + 'POST /api/crypto/decrypt': 'Decrypt text (AES/TripleDES/Rabbit/RC4)', + 'POST /api/crypto/rsa/generate': 'Generate RSA key pair', + 'POST /api/crypto/bip39/generate': 'Generate BIP39 mnemonic', + 'POST /api/crypto/bip39/to-entropy': 'Convert BIP39 mnemonic to entropy', + 'POST /api/crypto/otp/secret': 'Generate OTP secret', + 'POST /api/crypto/otp/totp/generate': 'Generate TOTP code', + 'POST /api/crypto/otp/totp/verify': 'Verify TOTP code', + 'POST /api/crypto/otp/hotp/generate': 'Generate HOTP code', + 'POST /api/crypto/otp/hotp/verify': 'Verify HOTP code', + 'POST /api/crypto/otp/key-uri': 'Build OTP key URI', + 'POST /api/crypto/password-strength': 'Analyse password strength', + }, + converter: { + 'POST /api/converter/base64/encode': 'Encode text to Base64', + 'POST /api/converter/base64/decode': 'Decode Base64 to text', + 'POST /api/converter/case': 'Convert text to all case formats', + 'POST /api/converter/roman-numeral/to-roman': 'Convert arabic number to roman numeral', + 'POST /api/converter/roman-numeral/to-arabic': 'Convert roman numeral to arabic number', + 'POST /api/converter/yaml-to-json': 'Convert YAML to JSON', + 'POST /api/converter/json-to-yaml': 'Convert JSON to YAML', + 'POST /api/converter/yaml-to-toml': 'Convert YAML to TOML', + 'POST /api/converter/json-to-toml': 'Convert JSON to TOML', + 'POST /api/converter/toml-to-json': 'Convert TOML to JSON', + 'POST /api/converter/toml-to-yaml': 'Convert TOML to YAML', + 'POST /api/converter/xml-to-json': 'Convert XML to JSON', + 'POST /api/converter/json-to-xml': 'Convert JSON to XML', + 'POST /api/converter/markdown-to-html': 'Convert Markdown to HTML', + 'POST /api/converter/color': 'Convert color between formats (hex/rgb/hsl/hsv/hwb/lch/lab)', + 'POST /api/converter/text-to-binary': 'Convert text to ASCII binary', + 'POST /api/converter/binary-to-text': 'Convert ASCII binary to text', + 'POST /api/converter/text-to-unicode': 'Convert text to Unicode entities', + 'POST /api/converter/unicode-to-text': 'Convert Unicode entities to text', + 'POST /api/converter/text-to-nato': 'Convert text to NATO phonetic alphabet', + 'POST /api/converter/integer-base': 'Convert integer between bases (2-64)', + 'POST /api/converter/temperature': 'Convert temperature between scales', + 'POST /api/converter/slugify': 'Slugify a string', + }, + web: { + 'POST /api/web/url/encode': 'URL encode a string', + 'POST /api/web/url/decode': 'URL decode a string', + 'POST /api/web/url/parse': 'Parse a URL into components', + 'POST /api/web/basic-auth/generate': 'Generate Basic Auth header', + 'POST /api/web/jwt/parse': 'Parse and decode a JWT', + 'POST /api/web/html-entities/encode': 'Encode HTML entities', + 'POST /api/web/html-entities/decode': 'Decode HTML entities', + 'POST /api/web/safelink/decode': 'Decode a Microsoft SafeLinks URL', + 'POST /api/web/user-agent/parse': 'Parse a User-Agent string', + 'POST /api/web/mime-types/lookup': 'Look up MIME type or extension', + 'GET /api/web/http-status-codes': 'List all HTTP status codes', + 'POST /api/web/http-status-codes/lookup': 'Look up an HTTP status code', + }, + development: { + 'POST /api/development/json/prettify': 'Prettify JSON', + 'POST /api/development/json/minify': 'Minify JSON', + 'POST /api/development/json/to-csv': 'Convert JSON array to CSV', + 'POST /api/development/json/diff': 'Diff two JSON objects', + 'POST /api/development/sql/prettify': 'Format SQL', + 'POST /api/development/chmod/calculate': 'Calculate chmod octal and symbolic', + 'POST /api/development/docker/to-compose': 'Convert docker run to docker-compose', + 'POST /api/development/xml/format': 'Format XML', + 'POST /api/development/yaml/format': 'Validate and format YAML', + 'POST /api/development/email/normalize': 'Normalize email addresses', + 'POST /api/development/regex/test': 'Test a regex against a string', + 'GET /api/development/port/random': 'Generate a random port number', + }, + network: { + 'POST /api/network/ipv4/subnet': 'IPv4 subnet calculator (CIDR)', + 'POST /api/network/ipv4/convert': 'Convert IPv4 to decimal/hex/binary/IPv6', + 'POST /api/network/ipv4/range': 'Expand IPv4 range to CIDR', + 'POST /api/network/mac/generate': 'Generate random MAC addresses', + 'POST /api/network/mac/lookup': 'Look up MAC address vendor (OUI)', + 'POST /api/network/ipv6/ula': 'Generate IPv6 ULA address', + }, + math: { + 'POST /api/math/evaluate': 'Evaluate a math expression', + 'POST /api/math/percentage': 'Calculate percentages', + }, + text: { + 'POST /api/text/lorem-ipsum': 'Generate lorem ipsum text', + 'POST /api/text/statistics': 'Get statistics for a text', + 'POST /api/text/numeronym': 'Generate numeronym(s) for words', + 'POST /api/text/obfuscate': 'Obfuscate a string', + 'POST /api/text/diff': 'Diff two text blocks (line-level)', + }, + data: { + 'POST /api/data/iban/validate': 'Validate and parse an IBAN', + 'POST /api/data/phone/parse': 'Parse and format a phone number', + 'GET /api/data/phone/countries': 'List supported phone country codes', + }, + }, + }); +}); + +// Serve built Vue frontend static files (when SERVE_STATIC=true) +if (process.env.SERVE_STATIC === 'true') { + app.use('/*', serveStatic({ root: './dist' })); + app.get('/*', serveStatic({ path: './dist/index.html' })); +} + +const port = Number(process.env.API_PORT ?? 3000); + +serve({ fetch: app.fetch, port }, () => { + console.log(`IT-Tools API running on http://localhost:${port}`); + console.log(`Endpoint reference: http://localhost:${port}/api`); +}); diff --git a/server/routes/converter.ts b/server/routes/converter.ts new file mode 100644 index 0000000000..825bc7c9fe --- /dev/null +++ b/server/routes/converter.ts @@ -0,0 +1,332 @@ +import { Hono } from 'hono'; +import { Base64 } from 'js-base64'; +import { + camelCase, capitalCase, constantCase, dotCase, + headerCase, noCase, paramCase, pascalCase, + pathCase, sentenceCase, snakeCase, +} from 'change-case'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; +import JSON5 from 'json5'; +import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'; +import convert from 'xml-js'; +import markdownit from 'markdown-it'; +import { colord } from 'colord'; +import slugify from '@sindresorhus/slugify'; +import { + arabicToRoman, + romanToArabic, +} from '@/tools/roman-numeral-converter/roman-numeral-converter.service.js'; +import { convertBase } from '@/tools/integer-base-converter/integer-base-converter.model.js'; +import { + convertTextToAsciiBinary, + convertAsciiBinaryToText, +} from '@/tools/text-to-binary/text-to-binary.models.js'; +import { + convertTextToUnicode, + convertUnicodeToText, +} from '@/tools/text-to-unicode/text-to-unicode.service.js'; +import { textToNatoAlphabet } from '@/tools/text-to-nato-alphabet/text-to-nato-alphabet.service.js'; +import { + convertCelsiusToKelvin, convertKelvinToCelsius, + convertFahrenheitToKelvin, convertKelvinToFahrenheit, + convertRankineToKelvin, convertKelvinToRankine, + convertDelisleToKelvin, convertKelvinToDelisle, + convertNewtonToKelvin, convertKelvinToNewton, + convertReaumurToKelvin, convertKelvinToReaumur, + convertRomerToKelvin, convertKelvinToRomer, +} from '@/tools/temperature-converter/temperature-converter.models.js'; + +const router = new Hono(); + +// Base64 encode +router.post('/base64/encode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', urlSafe = false } = body; + let result = Base64.encode(text); + if (urlSafe) result = result.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + return c.json({ result }); +}); + +// Base64 decode +router.post('/base64/decode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', urlSafe = false } = body; + try { + let clean = text.replace(/^data:.*?;base64,/, ''); + if (urlSafe) clean = clean.replace(/-/g, '+').replace(/_/g, '/'); + const result = Base64.decode(clean); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: `Invalid base64: ${e.message}` }, 400); + } +}); + +// Case converter +router.post('/case', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + const cfg = { stripRegexp: /[^A-Za-zÀ-ÖØ-öø-ÿ]+/gi }; + return c.json({ + result: { + lowercase: text.toLocaleLowerCase(), + uppercase: text.toLocaleUpperCase(), + camelCase: camelCase(text, cfg), + capitalCase: capitalCase(text, cfg), + constantCase: constantCase(text, cfg), + dotCase: dotCase(text, cfg), + headerCase: headerCase(text, cfg), + noCase: noCase(text, cfg), + paramCase: paramCase(text, cfg), + pascalCase: pascalCase(text, cfg), + pathCase: pathCase(text, cfg), + sentenceCase: sentenceCase(text, cfg), + snakeCase: snakeCase(text, cfg), + }, + }); +}); + +// Roman numeral: arabic → roman +router.post('/roman-numeral/to-roman', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { number } = body; + if (typeof number !== 'number') return c.json({ error: 'Provide a numeric "number" field' }, 400); + const result = arabicToRoman(number); + if (!result) return c.json({ error: 'Number must be between 1 and 3999' }, 400); + return c.json({ result }); +}); + +// Roman numeral: roman → arabic +router.post('/roman-numeral/to-arabic', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { roman = '' } = body; + const result = romanToArabic(roman.toUpperCase()); + if (result === null) return c.json({ error: 'Invalid roman numeral' }, 400); + return c.json({ result }); +}); + +// YAML → JSON +router.post('/yaml-to-json', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { yaml = '' } = body; + try { + const obj = parseYaml(yaml, { merge: true }); + return c.json({ result: obj }); + } catch (e: any) { + return c.json({ error: `Invalid YAML: ${e.message}` }, 400); + } +}); + +// JSON → YAML +router.post('/json-to-yaml', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { json = '' } = body; + try { + const obj = JSON5.parse(json); + return c.json({ result: stringifyYaml(obj) }); + } catch (e: any) { + return c.json({ error: `Invalid JSON: ${e.message}` }, 400); + } +}); + +// YAML → TOML +router.post('/yaml-to-toml', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { yaml = '' } = body; + try { + const obj = parseYaml(yaml, { merge: true }); + return c.json({ result: stringifyToml(obj as any) }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// JSON → TOML +router.post('/json-to-toml', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { json = '' } = body; + try { + const obj = JSON5.parse(json); + return c.json({ result: stringifyToml(obj) }); + } catch (e: any) { + return c.json({ error: `Invalid JSON: ${e.message}` }, 400); + } +}); + +// TOML → JSON +router.post('/toml-to-json', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { toml = '' } = body; + try { + const obj = parseToml(toml); + return c.json({ result: obj }); + } catch (e: any) { + return c.json({ error: `Invalid TOML: ${e.message}` }, 400); + } +}); + +// TOML → YAML +router.post('/toml-to-yaml', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { toml = '' } = body; + try { + const obj = parseToml(toml); + return c.json({ result: stringifyYaml(obj as any) }); + } catch (e: any) { + return c.json({ error: `Invalid TOML: ${e.message}` }, 400); + } +}); + +// XML → JSON +router.post('/xml-to-json', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { xml = '' } = body; + try { + const result = JSON.parse(convert.xml2json(xml, { compact: true, spaces: 2 })); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: `Invalid XML: ${e.message}` }, 400); + } +}); + +// JSON → XML +router.post('/json-to-xml', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { json = '' } = body; + try { + const obj = typeof json === 'string' ? JSON5.parse(json) : json; + const result = convert.json2xml(JSON.stringify(obj), { compact: true, spaces: 2 }); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: `Invalid JSON: ${e.message}` }, 400); + } +}); + +// Markdown → HTML +router.post('/markdown-to-html', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { markdown = '' } = body; + const md = markdownit(); + return c.json({ result: md.render(markdown) }); +}); + +// Color converter +router.post('/color', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { color = '' } = body; + try { + const parsed = colord(color); + if (!parsed.isValid()) return c.json({ error: 'Invalid color value' }, 400); + return c.json({ + result: { + hex: parsed.toHex(), + rgb: parsed.toRgb(), + hsl: parsed.toHsl(), + hsv: parsed.toHsv(), + }, + }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// Text → Binary +router.post('/text-to-binary', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + return c.json({ result: convertTextToAsciiBinary(text) }); +}); + +// Binary → Text +router.post('/binary-to-text', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { binary = '' } = body; + try { + return c.json({ result: convertAsciiBinaryToText(binary) }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// Text → Unicode +router.post('/text-to-unicode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + return c.json({ result: convertTextToUnicode(text) }); +}); + +// Unicode → Text +router.post('/unicode-to-text', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { unicode = '' } = body; + return c.json({ result: convertUnicodeToText(unicode) }); +}); + +// Text → NATO alphabet +router.post('/text-to-nato', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + return c.json({ result: textToNatoAlphabet({ text }) }); +}); + +// Integer base converter +router.post('/integer-base', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { value = '0', fromBase = 10, toBase = 2 } = body; + try { + const result = convertBase({ value: String(value), fromBase, toBase }); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// Temperature converter +router.post('/temperature', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { value, from = 'celsius' } = body; + if (typeof value !== 'number') return c.json({ error: 'Provide a numeric "value" field' }, 400); + + const toKelvin: Record number> = { + celsius: convertCelsiusToKelvin, + fahrenheit: convertFahrenheitToKelvin, + kelvin: (v) => v, + rankine: convertRankineToKelvin, + delisle: convertDelisleToKelvin, + newton: convertNewtonToKelvin, + reaumur: convertReaumurToKelvin, + romer: convertRomerToKelvin, + }; + + const fromKelvin: Record number> = { + celsius: convertKelvinToCelsius, + fahrenheit: convertKelvinToFahrenheit, + kelvin: (v) => v, + rankine: convertKelvinToRankine, + delisle: convertKelvinToDelisle, + newton: convertKelvinToNewton, + reaumur: convertKelvinToReaumur, + romer: convertKelvinToRomer, + }; + + const scale = from.toLowerCase(); + if (!toKelvin[scale]) { + return c.json({ error: `Unknown unit "${from}". Use: ${Object.keys(toKelvin).join(', ')}` }, 400); + } + + const kelvin = toKelvin[scale](value); + const result: Record = {}; + for (const unit of Object.keys(fromKelvin)) { + result[unit] = fromKelvin[unit](kelvin); + } + + return c.json({ result }); +}); + +// Slugify +router.post('/slugify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + return c.json({ result: slugify(text) }); +}); + +export default router; diff --git a/server/routes/crypto.ts b/server/routes/crypto.ts new file mode 100644 index 0000000000..005f92c25b --- /dev/null +++ b/server/routes/crypto.ts @@ -0,0 +1,315 @@ +import { Hono } from 'hono'; +import CryptoJSLib from 'crypto-js'; +import bcryptjs from 'bcryptjs'; +const { hashSync, compareSync } = bcryptjs; +import { v1, v3, v4, v5, NIL } from 'uuid'; +import { ulid } from 'ulid'; +import nodeForge from 'node-forge'; +const { pki } = nodeForge; +import bip39Lib from '@it-tools/bip39'; +const { entropyToMnemonic, mnemonicToEntropy, generateEntropy, englishWordList } = bip39Lib as any; +import { createToken } from '@/tools/token-generator/token-generator.service.js'; +import { + getPasswordCrackTimeEstimation, + getCharsetLength, +} from '@/tools/password-strength-analyser/password-strength-analyser.service.js'; + +// crypto-js is CJS-only; destructure from the default export +const CryptoJS = CryptoJSLib as any; +const { MD5, SHA1, SHA224, SHA256, SHA384, SHA512, SHA3, RIPEMD160, enc } = CryptoJS; +const { HmacMD5, HmacSHA1, HmacSHA224, HmacSHA256, HmacSHA384, HmacSHA512, HmacSHA3, HmacRIPEMD160 } = CryptoJS; +const { AES, TripleDES, Rabbit, RC4 } = CryptoJS; + +// --- Inlined OTP helpers (avoids crypto-js named-import issue in ESM) --- + +function base32toHex(base32: string): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + const bits = base32.toUpperCase().replace(/=+$/, '').split('') + .map(v => chars.indexOf(v).toString(2).padStart(5, '0')).join(''); + return (bits.match(/.{1,8}/g) ?? []) + .map(chunk => Number.parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); +} + +function hexToBytes(hex: string): number[] { + return (hex.match(/.{1,2}/g) ?? []).map(c => Number.parseInt(c, 16)); +} + +function generateHOTP({ key, counter = 0 }: { key: string; counter?: number }): string { + const digest = HmacSHA1(CryptoJS.enc.Hex.parse(counter.toString(16).padStart(16, '0')), CryptoJS.enc.Hex.parse(base32toHex(key))).toString(CryptoJS.enc.Hex); + const bytes = hexToBytes(digest); + const offset = bytes[19] & 0xF; + const v = ((bytes[offset] & 0x7F) << 24) | ((bytes[offset + 1] & 0xFF) << 16) + | ((bytes[offset + 2] & 0xFF) << 8) | (bytes[offset + 3] & 0xFF); + return String(v % 1000000).padStart(6, '0'); +} + +function verifyHOTP({ token, key, window = 0, counter = 0 }: { token: string; key: string; window?: number; counter?: number }): boolean { + for (let i = counter - window; i <= counter + window; i++) { + if (generateHOTP({ key, counter: i }) === token) return true; + } + return false; +} + +function generateTOTP({ key, now = Date.now(), timeStep = 30 }: { key: string; now?: number; timeStep?: number }): string { + return generateHOTP({ key, counter: Math.floor(now / 1000 / timeStep) }); +} + +function verifyTOTP({ key, token, window = 0, now = Date.now(), timeStep = 30 }: { key: string; token: string; window?: number; now?: number; timeStep?: number }): boolean { + return verifyHOTP({ token, key, window, counter: Math.floor(now / 1000 / timeStep) }); +} + +function buildKeyUri({ secret, app = 'IT-Tools', account = 'demo-user', algorithm = 'SHA1', digits = 6, period = 30 }: { secret: string; app?: string; account?: string; algorithm?: string; digits?: number; period?: number }): string { + const params = { issuer: app, secret, algorithm, digits, period }; + const qs = Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&'); + return `otpauth://totp/${encodeURIComponent(app)}:${encodeURIComponent(account)}?${qs}`; +} + +function generateSecret(): string { + return createToken({ length: 16, alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' }); +} + +// --- + +const router = new Hono(); + +// Token generator +router.post('/token', async (c) => { + const body = await c.req.json().catch(() => ({})); + const token = createToken({ + withUppercase: body.withUppercase ?? true, + withLowercase: body.withLowercase ?? true, + withNumbers: body.withNumbers ?? true, + withSymbols: body.withSymbols ?? false, + length: body.length ?? 64, + alphabet: body.alphabet, + }); + return c.json({ result: token }); +}); + +// Hash text +router.post('/hash', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', algorithm = 'SHA256', encoding = 'Hex' } = body; + + const algos: Record any> = { + MD5, SHA1, SHA224, SHA256, SHA384, SHA512, SHA3, RIPEMD160, + }; + + if (!algos[algorithm]) { + return c.json({ error: `Unsupported algorithm. Use one of: ${Object.keys(algos).join(', ')}` }, 400); + } + + const hash = algos[algorithm](text); + const encodings: Record = { Hex: enc.Hex, Base64: enc.Base64, Latin1: enc.Latin1 }; + const result = encoding === 'Bin' + ? hash.toString(enc.Hex).split('').map((b: string) => parseInt(b, 16).toString(2).padStart(4, '0')).join('') + : hash.toString(encodings[encoding] ?? enc.Hex); + + return c.json({ result }); +}); + +// HMAC +router.post('/hmac', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', secret = '', algorithm = 'SHA256', encoding = 'Hex' } = body; + + const algos: Record any> = { + MD5: HmacMD5, SHA1: HmacSHA1, SHA224: HmacSHA224, + SHA256: HmacSHA256, SHA384: HmacSHA384, SHA512: HmacSHA512, + SHA3: HmacSHA3, RIPEMD160: HmacRIPEMD160, + }; + + if (!algos[algorithm]) { + return c.json({ error: `Unsupported algorithm. Use one of: ${Object.keys(algos).join(', ')}` }, 400); + } + + const hash = algos[algorithm](text, secret); + const encodings: Record = { Hex: enc.Hex, Base64: enc.Base64 }; + const result = hash.toString(encodings[encoding] ?? enc.Hex); + return c.json({ result }); +}); + +// Bcrypt hash +router.post('/bcrypt/hash', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', saltRounds = 10 } = body; + const result = hashSync(text, saltRounds); + return c.json({ result }); +}); + +// Bcrypt verify +router.post('/bcrypt/verify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', hash = '' } = body; + try { + const match = compareSync(text, hash); + return c.json({ result: match }); + } catch { + return c.json({ error: 'Invalid hash format' }, 400); + } +}); + +// UUID generator +router.post('/uuid', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { version = 'v4', count = 1, namespace, name } = body; + + const generators: Record string> = { + NIL: () => NIL, + v1: (i) => v1({ clockseq: i, msecs: Date.now(), nsecs: Math.floor(Math.random() * 10000), node: Array.from({ length: 6 }, () => Math.floor(Math.random() * 256)) }), + v3: () => v3(name ?? '', namespace ?? '6ba7b811-9dad-11d1-80b4-00c04fd430c8'), + v4: () => v4(), + v5: () => v5(name ?? '', namespace ?? '6ba7b811-9dad-11d1-80b4-00c04fd430c8'), + }; + + if (!generators[version]) { + return c.json({ error: 'Unsupported version. Use one of: NIL, v1, v3, v4, v5' }, 400); + } + + try { + const result = Array.from({ length: Math.min(count, 100) }, (_, i) => generators[version](i)); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// ULID generator +router.post('/ulid', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { count = 1 } = body; + const result = Array.from({ length: Math.min(count, 100) }, () => ulid()); + return c.json({ result }); +}); + +// Encrypt +router.post('/encrypt', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', secret = '', algorithm = 'AES' } = body; + const algos: Record = { AES, TripleDES, Rabbit, RC4 }; + if (!algos[algorithm]) { + return c.json({ error: 'Unsupported algorithm. Use one of: AES, TripleDES, Rabbit, RC4' }, 400); + } + const result = algos[algorithm].encrypt(text, secret).toString(); + return c.json({ result }); +}); + +// Decrypt +router.post('/decrypt', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '', secret = '', algorithm = 'AES' } = body; + const algos: Record = { AES, TripleDES, Rabbit, RC4 }; + if (!algos[algorithm]) { + return c.json({ error: 'Unsupported algorithm. Use one of: AES, TripleDES, Rabbit, RC4' }, 400); + } + try { + const result = algos[algorithm].decrypt(text, secret).toString(enc.Utf8); + if (!result) return c.json({ error: 'Unable to decrypt — wrong secret or algorithm?' }, 400); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: `Decryption failed: ${e.message}` }, 400); + } +}); + +// RSA key pair generator +router.post('/rsa/generate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const bits = body.bits ?? 2048; + try { + const keyPair = await new Promise((resolve, reject) => { + pki.rsa.generateKeyPair({ bits }, (err, kp) => err ? reject(err) : resolve(kp)); + }); + return c.json({ + result: { + publicKey: pki.publicKeyToPem(keyPair.publicKey), + privateKey: pki.privateKeyToPem(keyPair.privateKey), + }, + }); + } catch (e: any) { + return c.json({ error: e.message }, 500); + } +}); + +// BIP39 — generate mnemonic +// entropyBits must be one of: 128, 160, 192, 224, 256 (maps to 16/20/24/28/32 bytes) +router.post('/bip39/generate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { entropyBits = 128 } = body; + const validBits = [128, 160, 192, 224, 256]; + if (!validBits.includes(entropyBits)) { + return c.json({ error: `entropyBits must be one of: ${validBits.join(', ')}` }, 400); + } + try { + const entropy = generateEntropy(entropyBits / 8); + const mnemonic = entropyToMnemonic(entropy, englishWordList); + return c.json({ result: mnemonic }); + } catch (e: any) { + return c.json({ error: e.message }, 500); + } +}); + +// BIP39 — mnemonic to entropy +router.post('/bip39/to-entropy', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { mnemonic = '' } = body; + try { + const entropy = mnemonicToEntropy(mnemonic, englishWordList); + return c.json({ result: entropy }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// OTP — generate secret +router.post('/otp/secret', async (c) => { + return c.json({ result: generateSecret() }); +}); + +// OTP — generate TOTP +router.post('/otp/totp/generate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { key = '', timeStep = 30 } = body; + const result = generateTOTP({ key, timeStep }); + return c.json({ result }); +}); + +// OTP — verify TOTP +router.post('/otp/totp/verify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { key = '', token = '', window = 0, timeStep = 30 } = body; + const result = verifyTOTP({ key, token, window, timeStep }); + return c.json({ result }); +}); + +// OTP — generate HOTP +router.post('/otp/hotp/generate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { key = '', counter = 0 } = body; + const result = generateHOTP({ key, counter }); + return c.json({ result }); +}); + +// OTP — verify HOTP +router.post('/otp/hotp/verify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { key = '', token = '', counter = 0, window = 0 } = body; + const result = verifyHOTP({ key, token, counter, window }); + return c.json({ result }); +}); + +// OTP — build key URI +router.post('/otp/key-uri', async (c) => { + const body = await c.req.json().catch(() => ({})); + const result = buildKeyUri(body); + return c.json({ result }); +}); + +// Password strength analyser +router.post('/password-strength', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { password = '' } = body; + const result = getPasswordCrackTimeEstimation({ password }); + return c.json({ result }); +}); + +export default router; diff --git a/server/routes/data.ts b/server/routes/data.ts new file mode 100644 index 0000000000..ac4e916401 --- /dev/null +++ b/server/routes/data.ts @@ -0,0 +1,61 @@ +import { Hono } from 'hono'; +import { isValidIBAN, validateIBAN, extractIBAN, electronicFormatIBAN, friendlyFormatIBAN } from 'ibantools'; +import { parsePhoneNumber, isValidPhoneNumber, getCountries, getCountryCallingCode } from 'libphonenumber-js/max'; +import { getFriendlyErrors } from '@/tools/iban-validator-and-parser/iban-validator-and-parser.service.js'; + +const router = new Hono(); + +// IBAN validator and parser +router.post('/iban/validate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { iban = '' } = body; + const clean = iban.replace(/\s/g, '').toUpperCase(); + const validation = validateIBAN(clean); + const extracted = isValidIBAN(clean) ? extractIBAN(clean) : null; + return c.json({ + result: { + isValid: validation.valid, + errors: getFriendlyErrors(validation.errorCodes), + electronicFormat: electronicFormatIBAN(clean) ?? null, + friendlyFormat: friendlyFormatIBAN(clean) ?? null, + bban: extracted?.bban ?? null, + countryCode: extracted?.countryCode ?? null, + }, + }); +}); + +// Phone number parser +router.post('/phone/parse', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { phone = '', defaultCountry } = body; + try { + const parsed = parsePhoneNumber(phone, defaultCountry as any); + return c.json({ + result: { + isValid: parsed.isValid(), + isPossible: parsed.isPossible(), + country: parsed.country, + countryCallingCode: parsed.countryCallingCode, + nationalNumber: parsed.nationalNumber, + number: parsed.number, + e164: parsed.format('E.164'), + international: parsed.format('INTERNATIONAL'), + national: parsed.format('NATIONAL'), + type: parsed.getType(), + }, + }); + } catch (e: any) { + return c.json({ error: `Invalid phone number: ${e.message}` }, 400); + } +}); + +// List supported countries (for phone) +router.get('/phone/countries', (c) => { + const result = getCountries().map((code) => ({ + code, + callingCode: `+${getCountryCallingCode(code)}`, + })); + return c.json({ result }); +}); + +export default router; diff --git a/server/routes/development.ts b/server/routes/development.ts new file mode 100644 index 0000000000..083c579fab --- /dev/null +++ b/server/routes/development.ts @@ -0,0 +1,172 @@ +import { Hono } from 'hono'; +import JSON5 from 'json5'; +import { format as formatSQL } from 'sql-formatter'; +import { composerize } from 'composerize-ts'; +import { normalizeEmail } from 'email-normalizer'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; +import { formatXml } from '@/tools/xml-formatter/xml-formatter.service.js'; +import { convertArrayToCsv } from '@/tools/json-to-csv/json-to-csv.service.js'; +import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from '@/tools/chmod-calculator/chmod-calculator.service.js'; + +const router = new Hono(); + +// JSON prettify +router.post('/json/prettify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { json = '', indent = 2 } = body; + try { + const parsed = JSON5.parse(json); + return c.json({ result: JSON.stringify(parsed, null, indent) }); + } catch (e: any) { + return c.json({ error: `Invalid JSON: ${e.message}` }, 400); + } +}); + +// JSON minify +router.post('/json/minify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { json = '' } = body; + try { + const parsed = JSON5.parse(json); + return c.json({ result: JSON.stringify(parsed) }); + } catch (e: any) { + return c.json({ error: `Invalid JSON: ${e.message}` }, 400); + } +}); + +// JSON → CSV +router.post('/json/to-csv', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { json = '' } = body; + try { + const parsed = typeof json === 'string' ? JSON5.parse(json) : json; + if (!Array.isArray(parsed)) return c.json({ error: 'JSON must be an array of objects' }, 400); + const result = convertArrayToCsv({ array: parsed }); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// JSON diff +router.post('/json/diff', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { left = '', right = '' } = body; + try { + const leftObj = JSON5.parse(left); + const rightObj = JSON5.parse(right); + const leftStr = JSON.stringify(leftObj, null, 2); + const rightStr = JSON.stringify(rightObj, null, 2); + return c.json({ result: { left: leftStr, right: rightStr, identical: leftStr === rightStr } }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// SQL prettify +router.post('/sql/prettify', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { sql = '', language = 'sql', keywordCase = 'upper' } = body; + try { + const result = formatSQL(sql, { language: language as any, keywordCase: keywordCase as any }); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// Chmod calculator +router.post('/chmod/calculate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { + owner = { read: false, write: false, execute: false }, + group = { read: false, write: false, execute: false }, + public: pub, + others, + } = body; + const pubPerms = pub ?? others ?? { read: false, write: false, execute: false }; + const permissions = { owner, group, public: pubPerms }; + const octal = computeChmodOctalRepresentation({ permissions }); + const symbolic = computeChmodSymbolicRepresentation({ permissions }); + return c.json({ result: { octal, symbolic, command: `chmod ${octal}` } }); +}); + +// Docker run → Docker Compose +router.post('/docker/to-compose', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { dockerRun, command } = body; + const cmd = (dockerRun ?? command ?? '').trim(); + try { + const { yaml, messages } = composerize(cmd); + return c.json({ result: { yaml, messages } }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// XML format +router.post('/xml/format', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { xml = '', indentSize = 2 } = body; + try { + const result = formatXml(xml, { indentation: ' '.repeat(indentSize) }); + if (!result && xml.trim()) return c.json({ error: 'Invalid XML' }, 400); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// YAML format/validate +router.post('/yaml/format', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { yaml = '' } = body; + try { + const parsed = parseYaml(yaml); + return c.json({ result: stringifyYaml(parsed) }); + } catch (e: any) { + return c.json({ error: `Invalid YAML: ${e.message}` }, 400); + } +}); + +// Email normalizer +router.post('/email/normalize', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { emails = '' } = body; + const lines = typeof emails === 'string' ? emails.split('\n') : (Array.isArray(emails) ? emails : [emails]); + const result = lines.map((email: string) => { + try { + return { original: email, normalized: normalizeEmail({ email: email.trim() }) }; + } catch { + return { original: email, normalized: null, error: 'Unable to parse email' }; + } + }); + return c.json({ result }); +}); + +// Regex tester +router.post('/regex/test', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { pattern = '', flags = 'g', text = '' } = body; + try { + const regex = new RegExp(pattern, flags); + const matches: { match: string; index: number; groups: Record | undefined }[] = []; + let m: RegExpExecArray | null; + const re = new RegExp(pattern, flags.includes('g') ? flags : flags + 'g'); + while ((m = re.exec(text)) !== null) { + matches.push({ match: m[0], index: m.index, groups: m.groups }); + if (!flags.includes('g')) break; + } + return c.json({ result: { matches, count: matches.length, isValid: true } }); + } catch (e: any) { + return c.json({ error: `Invalid regex: ${e.message}` }, 400); + } +}); + +// Random port +router.get('/port/random', (c) => { + const port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024; + return c.json({ result: port }); +}); + +export default router; diff --git a/server/routes/math.ts b/server/routes/math.ts new file mode 100644 index 0000000000..2db4618517 --- /dev/null +++ b/server/routes/math.ts @@ -0,0 +1,54 @@ +import { Hono } from 'hono'; +import { evaluate } from 'mathjs'; + +const router = new Hono(); + +// Math evaluator +router.post('/evaluate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { expression = '' } = body; + try { + const result = evaluate(expression); + return c.json({ result: result?.toString() ?? '' }); + } catch (e: any) { + return c.json({ error: `Invalid expression: ${e.message}` }, 400); + } +}); + +// Percentage calculator +router.post('/percentage', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { type, x, y } = body; + + if (typeof x !== 'number' || typeof y !== 'number') { + return c.json({ error: 'Provide numeric "x" and "y" fields' }, 400); + } + + if (type === 'percent-of') { + // What is x% of y? + return c.json({ result: (x / 100) * y }); + } + + if (type === 'is-what-percent') { + // x is what percent of y? + if (y === 0) return c.json({ error: 'Cannot divide by zero' }, 400); + return c.json({ result: (100 * x) / y }); + } + + if (type === 'percent-change') { + // Percentage change from x to y + if (x === 0) return c.json({ error: 'Cannot divide by zero' }, 400); + return c.json({ result: ((y - x) / x) * 100 }); + } + + // Default: return all three calculations + return c.json({ + result: { + percentOf: (x / 100) * y, + isWhatPercent: y !== 0 ? (100 * x) / y : null, + percentChange: x !== 0 ? ((y - x) / x) * 100 : null, + }, + }); +}); + +export default router; diff --git a/server/routes/network.ts b/server/routes/network.ts new file mode 100644 index 0000000000..303c979e6e --- /dev/null +++ b/server/routes/network.ts @@ -0,0 +1,97 @@ +import { Hono } from 'hono'; +import CryptoJSLib from 'crypto-js'; +const { SHA1 } = CryptoJSLib as any; +import db from 'oui-data'; +import { Netmask } from 'netmask'; +import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from '@/tools/ipv4-address-converter/ipv4-address-converter.service.js'; +import { calculateCidr } from '@/tools/ipv4-range-expander/ipv4-range-expander.service.js'; +import { getIPClass } from '@/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.models.js'; +import { generateRandomMacAddress } from '@/tools/mac-address-generator/mac-adress-generator.models.js'; + +const router = new Hono(); + +// IPv4 subnet calculator +router.post('/ipv4/subnet', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { cidr = '' } = body; + try { + const block = new Netmask(cidr); + const ip = cidr.split('/')[0]; + return c.json({ + result: { + networkAddress: block.base, + broadcastAddress: block.broadcast, + subnetMask: block.mask, + wildcardMask: block.hostmask, + firstHost: block.first, + lastHost: block.last, + totalHosts: block.size, + usableHosts: Math.max(block.size - 2, 0), + ipClass: getIPClass({ ip }), + cidr: `${block.base}/${block.bitmask}`, + }, + }); + } catch (e: any) { + return c.json({ error: `Invalid CIDR: ${e.message}` }, 400); + } +}); + +// IPv4 address converter +router.post('/ipv4/convert', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { ip = '' } = body; + if (!isValidIpv4({ ip })) return c.json({ error: 'Invalid IPv4 address' }, 400); + const decimal = ipv4ToInt({ ip }); + const ipv6 = ipv4ToIpv6({ ip }); + const hex = decimal.toString(16).padStart(8, '0').toUpperCase(); + const binary = decimal.toString(2).padStart(32, '0').match(/.{8}/g)!.join('.'); + return c.json({ result: { dotDecimal: ip, decimal, hex, binary, ipv6 } }); +}); + +// IPv4 range expander +router.post('/ipv4/range', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { startIp = '', endIp = '' } = body; + if (!isValidIpv4({ ip: startIp })) return c.json({ error: 'Invalid start IP' }, 400); + if (!isValidIpv4({ ip: endIp })) return c.json({ error: 'Invalid end IP' }, 400); + const result = calculateCidr({ startIp, endIp }); + if (!result) return c.json({ error: 'Could not calculate CIDR for given range' }, 400); + return c.json({ result }); +}); + +// MAC address generator +router.post('/mac/generate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { count = 1, prefix = '', separator = ':' } = body; + const result = Array.from({ length: Math.min(count, 100) }, () => + generateRandomMacAddress({ prefix, separator }), + ); + return c.json({ result }); +}); + +// MAC address lookup (OUI) +router.post('/mac/lookup', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { mac = '' } = body; + const key = mac.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6); + const vendor = (db as Record)[key]; + return c.json({ result: { mac, vendor: vendor ?? null, found: !!vendor } }); +}); + +// IPv6 ULA generator +router.post('/ipv6/ula', async (c) => { + const body = await c.req.json().catch(() => ({})); + const mac = body.mac ?? '00:00:00:00:00:00'; + const timestamp = Date.now(); + const hex40bit = SHA1(timestamp + mac).toString().substring(30); + const ula = `fd${hex40bit.substring(0, 2)}:${hex40bit.substring(2, 6)}:${hex40bit.substring(6)}`; + return c.json({ + result: { + ula: `${ula}::/48`, + firstRoutableBlock: `${ula}:0::/64`, + lastRoutableBlock: `${ula}:ffff::/64`, + }, + }); +}); + +export default router; diff --git a/server/routes/text.ts b/server/routes/text.ts new file mode 100644 index 0000000000..1cf1e69dc8 --- /dev/null +++ b/server/routes/text.ts @@ -0,0 +1,80 @@ +import { Hono } from 'hono'; +import { generateLoremIpsum } from '@/tools/lorem-ipsum-generator/lorem-ipsum-generator.service.js'; +import { getStringSizeInBytes } from '@/tools/text-statistics/text-statistics.service.js'; +import { generateNumeronym } from '@/tools/numeronym-generator/numeronym-generator.service.js'; +import { obfuscateString } from '@/tools/string-obfuscator/string-obfuscator.model.js'; + +const router = new Hono(); + +// Lorem ipsum generator +router.post('/lorem-ipsum', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { + paragraphCount = 1, + sentencePerParagraph = 3, + wordCount = 10, + startWithLoremIpsum = true, + asHTML = false, + } = body; + const result = generateLoremIpsum({ paragraphCount, sentencePerParagraph, wordCount, startWithLoremIpsum, asHTML }); + return c.json({ result }); +}); + +// Text statistics +router.post('/statistics', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + const words = text.trim() === '' ? [] : text.trim().split(/\s+/); + const sentences = text.trim() === '' ? [] : text.split(/[.!?]+/).filter((s: string) => s.trim()); + const lines = text.split('\n'); + return c.json({ + result: { + characters: text.length, + charactersNoSpaces: text.replace(/\s/g, '').length, + words: words.length, + sentences: sentences.length, + lines: lines.length, + paragraphs: text.split(/\n\s*\n/).filter((p: string) => p.trim()).length, + bytes: getStringSizeInBytes(text), + }, + }); +}); + +// Numeronym generator +router.post('/numeronym', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + const words = text.split(/\s+/).filter(Boolean); + if (words.length === 1) { + return c.json({ result: generateNumeronym(words[0]) }); + } + const result = words.map((w: string) => generateNumeronym(w)).join(' '); + return c.json({ result }); +}); + +// String obfuscator +router.post('/obfuscate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { + text = '', + keepFirst = 4, + keepLast = 0, + keepSpace = true, + replacementChar = '*', + } = body; + const result = obfuscateString(text, { keepFirst, keepLast, keepSpace, replacementChar }); + return c.json({ result }); +}); + +// Text diff (line-level) +router.post('/diff', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { left = '', right = '' } = body; + const leftLines = left.split('\n'); + const rightLines = right.split('\n'); + const added = rightLines.filter((l: string) => !leftLines.includes(l)).length; + const removed = leftLines.filter((l: string) => !rightLines.includes(l)).length; + return c.json({ result: { identical: left === right, addedLines: added, removedLines: removed } }); +}); + +export default router; diff --git a/server/routes/web.ts b/server/routes/web.ts new file mode 100644 index 0000000000..702e8aa491 --- /dev/null +++ b/server/routes/web.ts @@ -0,0 +1,177 @@ +import { Hono } from 'hono'; +import lodash from 'lodash'; +const { escape, unescape } = lodash; +import uaParserJs from 'ua-parser-js'; +const UAParser = (uaParserJs as any).UAParser ?? uaParserJs; +import mime from 'mime-types'; +import { decodeJwt } from '@/tools/jwt-parser/jwt-parser.service.js'; +import { decodeSafeLinksURL } from '@/tools/safelink-decoder/safelink-decoder.service.js'; +import { textToBase64 } from '@/utils/base64.js'; + +const router = new Hono(); + +// HTTP status codes reference +const HTTP_STATUS_CODES: Record = { + 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', + 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', + 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', + 208: 'Already Reported', 226: 'IM Used', + 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', + 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', + 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', + 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', + 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', + 413: 'Content Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: "I'm a Teapot", + 421: 'Misdirected Request', 422: 'Unprocessable Content', 423: 'Locked', + 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', + 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', + 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', + 510: 'Not Extended', 511: 'Network Authentication Required', +}; + +// URL encode +router.post('/url/encode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + try { + return c.json({ result: encodeURIComponent(text) }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// URL decode +router.post('/url/decode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + try { + return c.json({ result: decodeURIComponent(text) }); + } catch (e: any) { + return c.json({ error: `Invalid encoded string: ${e.message}` }, 400); + } +}); + +// URL parser +router.post('/url/parse', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { url = '' } = body; + try { + const parsed = new URL(url); + const params: Record = {}; + parsed.searchParams.forEach((v, k) => { params[k] = v; }); + return c.json({ + result: { + protocol: parsed.protocol, + username: parsed.username, + password: parsed.password, + hostname: parsed.hostname, + port: parsed.port, + pathname: parsed.pathname, + search: parsed.search, + params, + hash: parsed.hash, + href: parsed.href, + origin: parsed.origin, + }, + }); + } catch (e: any) { + return c.json({ error: `Invalid URL: ${e.message}` }, 400); + } +}); + +// Basic auth generator +router.post('/basic-auth/generate', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { username = '', password = '' } = body; + const encoded = textToBase64(`${username}:${password}`); + return c.json({ + result: { + header: `Authorization: Basic ${encoded}`, + token: encoded, + }, + }); +}); + +// JWT parser +router.post('/jwt/parse', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { jwt = '' } = body; + try { + const result = decodeJwt({ jwt }); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: `Invalid JWT: ${e.message}` }, 400); + } +}); + +// HTML entities encode +router.post('/html-entities/encode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + return c.json({ result: escape(text) }); +}); + +// HTML entities decode +router.post('/html-entities/decode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { text = '' } = body; + return c.json({ result: unescape(text) }); +}); + +// Safelink decoder +router.post('/safelink/decode', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { url = '' } = body; + try { + const result = decodeSafeLinksURL(url); + return c.json({ result }); + } catch (e: any) { + return c.json({ error: e.message }, 400); + } +}); + +// User agent parser +router.post('/user-agent/parse', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { userAgent = '' } = body; + if (!userAgent.trim()) { + return c.json({ result: { ua: '', browser: {}, cpu: {}, device: {}, engine: {}, os: {} } }); + } + const result = UAParser(userAgent.trim()); + return c.json({ result }); +}); + +// MIME types — lookup +router.post('/mime-types/lookup', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { query = '' } = body; + const mimeType = mime.lookup(query); + const ext = mime.extension(query); + return c.json({ result: { mimeType: mimeType || null, extension: ext || null } }); +}); + +// HTTP status codes — list all +router.get('/http-status-codes', (c) => { + const result = Object.entries(HTTP_STATUS_CODES).map(([code, message]) => ({ + code: Number(code), + message, + category: Math.floor(Number(code) / 100) * 100, + })); + return c.json({ result }); +}); + +// HTTP status codes — lookup +router.post('/http-status-codes/lookup', async (c) => { + const body = await c.req.json().catch(() => ({})); + const { code } = body; + const message = HTTP_STATUS_CODES[code]; + if (!message) return c.json({ error: `Unknown status code: ${code}` }, 404); + return c.json({ result: { code, message } }); +}); + +export default router; diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000000..bc7cdbfce2 --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ES2022"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Node", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": false + }, + "include": ["server/**/*", "src/**/*.ts"], + "exclude": ["src/**/*.vue", "src/**/*.test.ts", "src/**/*.spec.ts"] +}