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"]
+}