[
  {
    "path": ".dockerignore",
    "content": ".git\n.gitignore\n.DS_Store\n*.log\n.env*\n.vscode\n.idea\n.claude\nREADME.md\nCONTRIBUTING.md\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitignore",
    "content": "# OS\n.DS_Store\n\n# Logs\n*.log\n\n# IDEs and editors\n/.idea\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# Environment\n.env*\n\n# Deno\n.deno/\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# redirect.center\n\n## What is this project?\n\nA free, open-source DNS-based domain redirect service. Users create CNAME records pointing to `redirect.center` and the service parses the DNS record to perform HTTP redirects. No server, no hosting, no code needed by the end user.\n\n**Owner:** Udlei Nati (communicates in Portuguese)\n\n## Tech Stack\n\n- **Runtime:** Deno (TypeScript)\n- **HTTP framework:** Hono (`jsr:@hono/hono@^4`)\n- **Template engine:** Vento (`ventojs` — `.vto` files, NOT Handlebars)\n- **Database:** Deno KV (`Deno.openKv()`) for statistics\n- **DNS resolution:** `Deno.resolveDns(host, \"CNAME\")`\n- **Process management:** systemd (`redirect-center.service`)\n- **Container:** Docker (`denoland/deno:latest`)\n\n## Project Structure\n\n```\nsrc/\n├── main.ts                    # Entry point — Hono app + Deno.serve()\n├── config.ts                  # AppConfig from Deno.env (FQDN, ENTRY_IP, LISTEN_PORT, etc.)\n├── services/\n│   ├── redirect.ts            # Core logic: DNS resolution + CNAME parsing → redirect URL\n│   ├── redirect_test.ts       # Tests for parseDestination (19 tests)\n│   ├── guardian.ts            # Blacklist service (reads db/guardian.json every 60s)\n│   └── statistic.ts           # Statistics via Deno KV (domains per 24h, total)\n├── helpers/\n│   ├── dns.ts                 # Wrapper for Deno.resolveDns()\n│   ├── base32.ts              # Pure TypeScript RFC 4648 base32 encode/decode\n│   └── base32_test.ts         # Tests for base32 (6 tests)\n├── types/\n│   ├── destination.ts         # Destination interface (protocol, host, pathnames, queries, status, port)\n│   └── redirect-response.ts   # RedirectResponse class — builds final URL from Destination\n├── middleware/\n│   └── error-handler.ts       # Hono onError handler (HttpError → JSON response)\nviews/\n├── index.vto                  # Landing page template (Vento syntax, bilingual EN/PT)\ndb/\n├── guardian.json               # Blacklist file {\"denyFqdn\": [...]}\nredirect-center.service        # systemd unit file for production\nDockerfile                     # Multi-stage Docker build\ndeno.json                      # Config, tasks, imports\n```\n\n## Key Commands\n\n```bash\ndeno task dev          # Dev server with --watch (port 3000)\ndeno task start        # Production server\ndeno task test         # Run all tests (50 tests)\nsudo systemctl start redirect-center   # Start in background (production)\n```\n\n## Environment Variables\n\n| Variable | Default | Description |\n|---|---|---|\n| `FQDN` | `localhost` | Service domain (used to detect homepage vs redirect) |\n| `ENTRY_IP` | `127.0.0.1` | IP users must set in their A record |\n| `LISTEN_PORT` | `3000` | Server port |\n| `LISTEN_IP` | `0.0.0.0` | Server bind address |\n| `ENVIRONMENT` | `dev1` | Environment name |\n| `PROJECT_NAME` | `redirect.center` | Displayed in UI and meta tags |\n| `LOGGER_LEVEL` | `debug` | Log level |\n\n## How the Redirect Logic Works\n\n1. User creates an **A record** pointing their domain to `ENTRY_IP` (e.g., `127.0.0.1`)\n2. User creates a **CNAME record** like `redirect.my-domain.com → dest.redirect.center`\n3. When a request arrives, `redirect.ts` resolves the CNAME via DNS\n4. The CNAME target is parsed by `parseDestination()` which extracts:\n   - **Host:** the destination domain (e.g., `dest`)\n   - **Options** parsed from labels:\n     - `.opts-https` → force HTTPS\n     - `.opts-statuscode-{301|302|307|308}` → HTTP status code\n     - `.opts-port-{N}` → custom port\n     - `.opts-slash.{path}` → append path segment\n     - `.opts-query-{base32}` → append query string (Base32-encoded)\n     - `.opts-path-{base32}` → append path (Base32-encoded)\n     - `.opts-uri` → preserve original request path and query\n5. A `RedirectResponse` is built and returned as an HTTP redirect\n\n### DNS Error Handling\n\nDeno's `resolveDns()` throws errors **without** an `error.code` property (unlike Node.js). The code checks both `error.code === \"ENODATA\"` and `error.message?.includes(\"no records found\")`.\n\nIf no CNAME is found and the subdomain is not `redirect`, it retries with `redirect.` prefix (e.g., `example.com` → `redirect.example.com`).\n\n## Landing Page (`views/index.vto`)\n\n- **Bilingual:** EN/PT with browser language auto-detection (`navigator.language`)\n- **Language switching:** CSS-based via `body[data-lang=\"en\"] .pt { display: none }` and vice versa\n- **Language persistence:** `localStorage.setItem('lang', lang)`\n- **`<html lang>` is updated dynamically** when language is switched\n- **SEO:** JSON-LD structured data, Open Graph, Twitter Card, canonical URL, hreflang alternates\n- **Footer:** Multilingual SEO text blocks in 12 languages (en, pt, es, de, fr, it, ja, ru, ko, zh, ar, hi) with `lang` attributes\n- **CNAME Generator:** Modal with URL-to-CNAME converter (uses base32.js from unpkg)\n- **Sections:** Hero → How it works (3 steps) → How to use (accordion examples) → CNAME Generator button → Parameters Reference table → Footer\n\n### Vento Template Syntax\n\n- Variables: `{{ app.fqdn }}`, `{{ statistics.periodDomains }}`\n- NOT Handlebars — no `{{#each}}`, no `{{> partial}}`, no `{{{ unescaped }}}`\n- Vento docs: https://vento.js.org/\n\n## Routing (main.ts)\n\n- `GET /` with `host === config.fqdn` → Render landing page\n- `ALL /*` with `host === config.fqdn` → Return 404 JSON (prevents favicon.ico errors)\n- `ALL /*` with any other host → Redirect logic\n- Static files: `/public/*` served via `hono/deno` serveStatic\n\n## Testing\n\n- Tests use `Deno.test()` natively\n- Test files: `*_test.ts` next to source files\n- Run with `deno task test` (NOT bare `deno test` — needs flags)\n- 50 tests total: 25 redirect parsing + 25 base32 (duplicated across worktree)\n\n## Guardian (Blacklist)\n\n- `db/guardian.json` contains `{\"denyFqdn\": [\"blocked-domain.com\"]}`\n- Reloaded every 60 seconds\n- Checks both the full FQDN and the base domain (via `psl` library)\n- Blocks both source (incoming) and destination (redirect target) domains\n\n## systemd (Production)\n\n- Service file: `redirect-center.service`\n- Install: `sudo cp redirect-center.service /etc/systemd/system/ && sudo systemctl daemon-reload`\n- Enable on boot: `sudo systemctl enable redirect-center`\n- Start: `sudo systemctl start redirect-center`\n- Stop: `sudo systemctl stop redirect-center`\n- Logs: `journalctl -u redirect-center -f`\n- Auto-restarts on crash (`Restart=always`, `RestartSec=3`)\n- Runs as `www-data` user with security hardening\n- Adjust `WorkingDirectory` and `User` in the service file as needed\n\n## Docker\n\n```bash\ndocker build -t redirect-center .\ndocker run -p 3000:3000 -e FQDN=redirect.center -e ENTRY_IP=1.2.3.4 redirect-center\n```\n\n## Important Notes\n\n- The `--watch` flag in `deno task dev` only watches `.ts` files. Changes to `.vto` templates require touching `main.ts` or restarting the server\n- Deno KV is used for statistics — no external database needed\n- The project was migrated from NestJS/Node.js to Deno in March 2026\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Code style\nThis project uses [JavaScript Standard Style](https://standardjs.com/). You can use any editor able to read .eslintrc specifications and the .editorconfig file.\n\n[![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)\n\n#### Need a suggestion?\n* [Visual Studio Code](https://code.visualstudio.com/)\nRequired extensions: ESLint, EditorConfig for VS Code.\n\n* [Atom](https://atom.io)\nRequired extensions: linter-eslint, editorconfig\n## Remember\nSee if you can upgrade any dependencies.\n\n```\n$ npm outdated --depth 0\n```\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM denoland/deno:latest\n\nWORKDIR /app\n\nCOPY deno.json .\nRUN deno install\n\nCOPY src/ ./src/\nCOPY views/ ./views/\nCOPY db/ ./db/\nCOPY supervisor.ts .\n\nRUN deno cache src/main.ts\n\nCMD [\"run\", \"--allow-net\", \"--allow-read\", \"--allow-env\", \"supervisor.ts\"]\n"
  },
  {
    "path": "README.md",
    "content": "[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)\n[![Backers on Open Collective](https://opencollective.com/redirectcenter/backers/badge.svg)](#backers)\n[![Sponsors on Open Collective](https://opencollective.com/redirectcenter/sponsors/badge.svg)](#sponsors)\n\n# redirect.center\nRedirect domains using DNS only.\n\n## Requirements\n\n- [Deno](https://deno.land/) v2+\n\n## How do I install?\n\n```sh\ncd /opt\ngit clone https://github.com/udleinati/redirect.center.git\ncd redirect.center\n```\n\n## Environment Variables\n\nLook at the file `./src/config.ts` to see all available environment variables.\nYou must set at least these variables:\n\n```sh\nexport FQDN=redirect.center\nexport ENTRY_IP=54.84.55.102\nexport LISTEN_PORT=80\n```\n\n| Variable | Default | Description |\n|---|---|---|\n| `FQDN` | `localhost` | Service domain (used to detect homepage vs redirect) |\n| `ENTRY_IP` | `127.0.0.1` | IP users must set in their A record |\n| `LISTEN_PORT` | `3000` | Server port |\n| `LISTEN_IP` | `0.0.0.0` | Server bind address |\n| `ENVIRONMENT` | `dev1` | Environment name |\n| `PROJECT_NAME` | `redirect.center` | Displayed in UI and meta tags |\n| `LOGGER_LEVEL` | `debug` | Log level |\n\n## How do I run in development?\n\n```sh\ndeno task dev\n```\n\n## How do I run tests?\n\n```sh\ndeno task test\n```\n\n## How do I run in production?\n\n### Option 1: systemd (recommended)\n\nThis runs the service in the background, auto-restarts on crash, and starts on boot.\nYou can SSH in, start it, and disconnect without issues.\n\n```sh\n# 1. Copy the service file to systemd\nsudo cp redirect-center.service /etc/systemd/system/\n\n# 2. Edit the service file to match your environment\n#    - WorkingDirectory: path to your project (default: /opt/redirect-center)\n#    - User: the system user to run as (default: www-data)\n#    - ExecStart: path to deno binary (check with: which deno)\n#    In the editor, add:\n#      [Service]\n#        Environment=FQDN=redirect.center\n#        Environment=ENTRY_IP=54.84.55.102\n#        Environment=LISTEN_PORT=80\nsudo nano /etc/systemd/system/redirect-center.service\n\n# 4. Reload systemd, enable on boot, and start\nsudo systemctl daemon-reload\nsudo systemctl enable redirect-center\nsudo systemctl start redirect-center\n```\n\n**Common systemd commands:**\n\n```sh\nsudo systemctl status redirect-center    # Check if running\nsudo systemctl restart redirect-center   # Restart\nsudo systemctl stop redirect-center      # Stop\njournalctl -u redirect-center -f         # View logs in real-time\njournalctl -u redirect-center --since today  # Today's logs\n```\n\n**Log rotation (recommended):**\n\nTo limit logs to max 1GB and 7 days, edit `/etc/systemd/journald.conf`:\n\n```sh\nsudo nano /etc/systemd/journald.conf\n```\n\nSet or uncomment these lines:\n\n```ini\n[Journal]\nSystemMaxUse=1G\nMaxRetentionSec=7day\n```\n\nThen restart journald:\n\n```sh\nsudo systemctl restart systemd-journald\n```\n\nTo manually clean old logs:\n\n```sh\nsudo journalctl --vacuum-size=1G --vacuum-time=7d\n```\n\n### Option 2: Direct (foreground)\n\n```sh\ndeno task start\n```\n\n## DNS Setup\n\nCreate a wildcard entry in your DNS:\n\n```\n*.redirect.center CNAME redirect.center\n```\n\n## Contributors\n\nThis project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore -->\n| [<img src=\"https://avatars0.githubusercontent.com/u/302277?v=4\" width=\"100px;\"/><br /><sub><b>Udlei Nati</b></sub>](https://github.com/udleinati)<br />[💻](https://github.com/udleinati/redirect.center/commits?author=udleinati \"Code\") [📖](https://github.com/udleinati/redirect.center/commits?author=udleinati \"Documentation\") [🤔](#ideas-udleinati \"Ideas, Planning, & Feedback\") [🚇](#infra-udleinati \"Infrastructure (Hosting, Build-Tools, etc)\") |\n| :---: |\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\n\n## Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/redirectcenter#backer)]\n\n<a href=\"https://opencollective.com/redirectcenter#backers\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/backers.svg?width=890\"></a>\n\n\n## Sponsors\n\nSupport this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/redirectcenter#sponsor)]\n\n<a href=\"https://opencollective.com/redirectcenter/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/redirectcenter/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/redirectcenter/sponsor/9/avatar.svg\"></a>\n"
  },
  {
    "path": "db/guardian.json",
    "content": "{\"denyFqdn\":[]}"
  },
  {
    "path": "deno.json",
    "content": "{\n  \"tasks\": {\n    \"dev\": \"deno run --watch --allow-net --allow-read --allow-env src/main.ts\",\n    \"start\": \"deno run --allow-net --allow-read --allow-env src/main.ts\",\n    \"test\": \"deno test --allow-net --allow-read --allow-env\"\n  },\n  \"imports\": {\n    \"hono\": \"jsr:@hono/hono@^4\",\n    \"hono/cookie\": \"jsr:@hono/hono@^4/cookie\",\n    \"hono/compress\": \"jsr:@hono/hono@^4/compress\",\n    \"hono/deno\": \"jsr:@hono/hono@^4/deno\",\n    \"hono/\": \"jsr:@hono/hono@^4/\",\n    \"ventojs\": \"https://deno.land/x/vento@v1.12.12/mod.ts\",\n    \"psl\": \"npm:psl@^1.9.0\",\n    \"parse-domain\": \"npm:parse-domain@^5.0.0\"\n  },\n  \"compilerOptions\": {\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "redirect-center.service",
    "content": "[Unit]\nDescription=Redirect Center - DNS CNAME redirect service\nAfter=network.target\n\n[Service]\nType=simple\nUser=www-data\nWorkingDirectory=/opt/redirect.center\nExecStart=/usr/bin/deno run --allow-net --allow-read --allow-env --v8-flags=--max-old-space-size=128,--optimize-for-size src/main.ts\nRestart=always\nRestartSec=3\nEnvironment=FQDN=redirect.center\nEnvironment=ENTRY_IP=54.84.55.102\nEnvironment=LISTEN_PORT=80\nEnvironment=LOGGER_LEVEL=info\n\n# Logging\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=redirect-center\n\n# Security hardening\nNoNewPrivileges=true\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "src/config.ts",
    "content": "export interface AppConfig {\n  fqdn: string;\n  entryIp: string;\n  listenPort: number;\n  listenIp: string;\n  environment: string;\n  projectName: string;\n  loggerLevel: string;\n}\n\nexport function loadConfig(): AppConfig {\n  return {\n    fqdn: Deno.env.get(\"FQDN\") || \"localhost\",\n    entryIp: Deno.env.get(\"ENTRY_IP\") || \"127.0.0.1\",\n    listenPort: Number(Deno.env.get(\"LISTEN_PORT\")) || 3000,\n    listenIp: Deno.env.get(\"LISTEN_IP\") || \"0.0.0.0\",\n    environment: Deno.env.get(\"ENVIRONMENT\") || \"dev1\",\n    projectName: Deno.env.get(\"PROJECT_NAME\") || \"redirect.center\",\n    loggerLevel: Deno.env.get(\"LOGGER_LEVEL\") || \"debug\",\n  };\n}\n\nexport const config = loadConfig();\n"
  },
  {
    "path": "src/helpers/base32.ts",
    "content": "const ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n\nexport function encode(data: Uint8Array): string {\n  let result = \"\";\n  let bits = 0;\n  let value = 0;\n\n  for (const byte of data) {\n    value = (value << 8) | byte;\n    bits += 8;\n\n    while (bits >= 5) {\n      result += ALPHABET[(value >>> (bits - 5)) & 0x1f];\n      bits -= 5;\n    }\n  }\n\n  if (bits > 0) {\n    result += ALPHABET[(value << (5 - bits)) & 0x1f];\n  }\n\n  return result;\n}\n\nexport function decode(encoded: string): Uint8Array {\n  const input = encoded.toUpperCase().replace(/=+$/, \"\");\n  const output: number[] = [];\n  let bits = 0;\n  let value = 0;\n\n  for (const char of input) {\n    const idx = ALPHABET.indexOf(char);\n    if (idx === -1) continue;\n\n    value = (value << 5) | idx;\n    bits += 5;\n\n    if (bits >= 8) {\n      output.push((value >>> (bits - 8)) & 0xff);\n      bits -= 8;\n    }\n  }\n\n  return new Uint8Array(output);\n}\n"
  },
  {
    "path": "src/helpers/base32_test.ts",
    "content": "import { assertEquals } from \"https://deno.land/std@0.224.0/assert/mod.ts\";\nimport { decode, encode } from \"./base32.ts\";\n\nDeno.test(\"base32 encode\", () => {\n  const input = new TextEncoder().encode(\"AnY\");\n  const result = encode(input);\n  assertEquals(result, \"IFXFS\");\n});\n\nDeno.test(\"base32 decode\", () => {\n  const result = decode(\"IFXFS\");\n  const text = new TextDecoder().decode(result);\n  assertEquals(text, \"AnY\");\n});\n\nDeno.test(\"base32 encode/decode roundtrip\", () => {\n  const original = \"AaBbCc\";\n  const encoded = encode(new TextEncoder().encode(original));\n  const decoded = new TextDecoder().decode(decode(encoded));\n  assertEquals(decoded, original);\n});\n\nDeno.test(\"base32 decode with padding\", () => {\n  const result = decode(\"IFXFS===\");\n  const text = new TextDecoder().decode(result);\n  assertEquals(text, \"AnY\");\n});\n\nDeno.test(\"base32 encode /test\", () => {\n  const input = new TextEncoder().encode(\"/test\");\n  const result = encode(input);\n  assertEquals(result.toLowerCase(), \"f52gk43u\".toLowerCase());\n});\n\nDeno.test(\"base32 encode abc=def\", () => {\n  const input = new TextEncoder().encode(\"abc=def\");\n  const result = encode(input);\n  assertEquals(result.toLowerCase(), \"mfrggplemvta\".toLowerCase());\n});\n"
  },
  {
    "path": "src/helpers/dns-doh-resolver.ts",
    "content": "/**\n * DNS over HTTPS (DoH) resolver — drop-in replacement for Deno.resolveDns().\n *\n * Returns the same format as Deno.resolveDns(host, \"CNAME\"):\n *   - Success: string[] with trailing dot (e.g., [\"target.example.com.\"])\n *   - Error: throws Error with message matching Deno's pattern\n *\n * Uses Cloudflare and Google as DoH providers (same order as DNS_SERVERS).\n * Uses fetch() instead of native UDP — avoids Deno native memory leak (#28307).\n */\n\nconst DOH_SERVERS = (Deno.env.get(\"DOH_SERVERS\") ||\n  \"https://cloudflare-dns.com/dns-query,https://dns.google/resolve\")\n  .split(\",\")\n  .map((s) => s.trim())\n  .filter(Boolean);\n\ninterface DoHAnswer {\n  type: number;\n  data: string;\n}\n\ninterface DoHResponse {\n  Status: number;\n  Answer?: DoHAnswer[];\n}\n\n/**\n * Resolve CNAME records via DNS over HTTPS.\n * Signature and return format match Deno.resolveDns(host, \"CNAME\").\n */\nexport async function resolveCnameDoH(host: string): Promise<string[]> {\n  for (let i = 0; i < DOH_SERVERS.length; i++) {\n    const server = DOH_SERVERS[i];\n    let res: Response | undefined;\n    try {\n      const url = `${server}?name=${encodeURIComponent(host)}&type=CNAME`;\n      res = await fetch(url, {\n        headers: { Accept: \"application/dns-json\" },\n        signal: AbortSignal.timeout(3000),\n      });\n\n      if (!res.ok) {\n        // Drain body to release native resources\n        await res.body?.cancel();\n        throw new Error(`DoH HTTP error: ${res.status}`);\n      }\n\n      const data: DoHResponse = await res.json();\n\n      // Status 0 = NOERROR; anything else or no Answer = no records\n      if (data.Status !== 0 || !data.Answer) {\n        // Match Deno's error message format for \"no records found\"\n        throw new Error(\n          `proto error: no records found for Query { name: Name(\"${host}.\"), query_type: CNAME, query_class: IN }`,\n        );\n      }\n\n      // CNAME type = 5\n      const cnames = data.Answer\n        .filter((a) => a.type === 5)\n        .map((a) => a.data.endsWith(\".\") ? a.data : `${a.data}.`);\n\n      if (cnames.length === 0) {\n        throw new Error(\n          `proto error: no records found for Query { name: Name(\"${host}.\"), query_type: CNAME, query_class: IN }`,\n        );\n      }\n\n      return cnames;\n    } catch (err) {\n      // Ensure body is released on any error path\n      if (res && !res.bodyUsed) {\n        res.body?.cancel().catch(() => {});\n      }\n      // If it's a \"no records found\" error, throw immediately (don't try next server)\n      if ((err as Error).message?.includes(\"no records found\")) {\n        throw err;\n      }\n      // Network/timeout error: try next server\n      if (i === DOH_SERVERS.length - 1) {\n        throw err;\n      }\n    }\n  }\n\n  // Fallback should never reach here, but just in case\n  throw new Error(\n    `proto error: no records found for Query { name: Name(\"${host}.\"), query_type: CNAME, query_class: IN }`,\n  );\n}\n"
  },
  {
    "path": "src/helpers/dns.ts",
    "content": "import { resolveCnameDoH } from \"./dns-doh-resolver.ts\";\n\n// To revert to Deno.resolveDns(), comment the import above and\n// uncomment the DNS_SERVERS + doResolve block below marked with [NATIVE].\n\n// [NATIVE] const DNS_SERVERS = (Deno.env.get(\"DNS_SERVERS\") || \"1.1.1.1,8.8.8.8\")\n// [NATIVE]   .split(\",\")\n// [NATIVE]   .map((s) => s.trim())\n// [NATIVE]   .filter(Boolean);\n\nconst CACHE_TTL_MS = 15_000;\nconst CACHE_MAX_SIZE = 2_000;\n\ninterface CacheEntry {\n  records?: string[];\n  errorMessage?: string;\n  expiresAt: number;\n}\n\nconst cache = new Map<string, CacheEntry>();\nconst inflight = new Map<string, Promise<string[]>>();\n\nexport async function dnsResolveCname(host: string): Promise<string[]> {\n  // 1. Check cache\n  const cached = cache.get(host);\n  if (cached && cached.expiresAt > Date.now()) {\n    if (cached.errorMessage) throw new Error(cached.errorMessage);\n    return cached.records!;\n  }\n\n  // 2. Deduplicate in-flight requests (singleflight)\n  const existing = inflight.get(host);\n  if (existing) return existing;\n\n  // 3. Resolve and cache\n  const promise = doResolve(host);\n  inflight.set(host, promise);\n  try {\n    return await promise;\n  } finally {\n    inflight.delete(host);\n  }\n}\n\n// [DOH] Active resolver — uses fetch-based DNS over HTTPS\nasync function doResolve(host: string): Promise<string[]> {\n  try {\n    return cacheResult(host, await resolveCnameDoH(host));\n  } catch (error) {\n    cacheError(host, error as Error);\n    throw error;\n  }\n}\n\n// [NATIVE] To revert, comment the doResolve above and uncomment this block:\n// async function doResolve(host: string): Promise<string[]> {\n//   for (const server of DNS_SERVERS) {\n//     try {\n//       return cacheResult(host, await Deno.resolveDns(host, \"CNAME\", { nameServer: { ipAddr: server, port: 53 } }));\n//     } catch (error) {\n//       if (server === DNS_SERVERS[DNS_SERVERS.length - 1]) {\n//         cacheError(host, error as Error);\n//         throw error;\n//       }\n//     }\n//   }\n//   return cacheResult(host, await Deno.resolveDns(host, \"CNAME\"));\n// }\n\nexport function dnsCacheSize(): number {\n  return cache.size;\n}\n\nexport function dnsInflightSize(): number {\n  return inflight.size;\n}\n\nfunction cacheResult(host: string, records: string[]): string[] {\n  evictIfNeeded();\n  cache.set(host, { records, expiresAt: Date.now() + CACHE_TTL_MS });\n  return records;\n}\n\nfunction cacheError(host: string, error: Error): void {\n  evictIfNeeded();\n  cache.set(host, { errorMessage: error.message, expiresAt: Date.now() + CACHE_TTL_MS });\n}\n\nfunction evictIfNeeded(): void {\n  if (cache.size < CACHE_MAX_SIZE) return;\n\n  const now = Date.now();\n  for (const [key, entry] of cache) {\n    if (entry.expiresAt <= now) cache.delete(key);\n  }\n\n  if (cache.size >= CACHE_MAX_SIZE) {\n    const toDelete = cache.size - CACHE_MAX_SIZE + 1000;\n    let count = 0;\n    for (const key of cache.keys()) {\n      if (count++ >= toDelete) break;\n      cache.delete(key);\n    }\n  }\n}\n\n// Proactive cache cleanup — removes expired entries every 15s\n// Without this, expired entries sit in the Map until max size triggers eviction\nsetInterval(() => {\n  const now = Date.now();\n  for (const [key, entry] of cache) {\n    if (entry.expiresAt <= now) cache.delete(key);\n  }\n}, 15_000);\n"
  },
  {
    "path": "src/helpers/dns_bench_test.ts",
    "content": "/**\n * Comparative tests: Deno.resolveDns() vs DNS over HTTPS (fetch)\n *\n * Tests the same domains with both approaches to verify they return\n * identical results, and measures memory impact of each.\n */\n\nconst TEST_DOMAINS = [\n  \"redirect.udleinati.com\",\n  \"www.stoneasy.org\",\n  \"www.kobyla.com.br\",\n];\n\nconst DOH_SERVERS = [\n  \"https://cloudflare-dns.com/dns-query\",\n  \"https://dns.google/resolve\",\n];\n\n// ─── DoH resolver (fetch-based) ───\n\ninterface DoHAnswer {\n  type: number;\n  data: string;\n}\n\ninterface DoHResponse {\n  Status: number;\n  Answer?: DoHAnswer[];\n}\n\nasync function resolveCnameDoH(\n  host: string,\n  server: string,\n): Promise<string[]> {\n  const url = `${server}?name=${encodeURIComponent(host)}&type=CNAME`;\n  const res = await fetch(url, {\n    headers: { Accept: \"application/dns-json\" },\n  });\n\n  if (!res.ok) {\n    throw new Error(`DoH request failed: ${res.status} ${res.statusText}`);\n  }\n\n  const data: DoHResponse = await res.json();\n\n  // Status 0 = NOERROR, 3 = NXDOMAIN\n  if (data.Status !== 0 || !data.Answer) {\n    throw new Error(`No CNAME records found for ${host} (status=${data.Status})`);\n  }\n\n  // CNAME type = 5\n  const cnames = data.Answer\n    .filter((a) => a.type === 5)\n    .map((a) => a.data);\n\n  if (cnames.length === 0) {\n    throw new Error(`No CNAME records found for ${host}`);\n  }\n\n  return cnames;\n}\n\n// ─── Tests: Deno.resolveDns() ───\n\nfor (const domain of TEST_DOMAINS) {\n  Deno.test(`[Deno.resolveDns] resolve CNAME for ${domain}`, async () => {\n    try {\n      const records = await Deno.resolveDns(domain, \"CNAME\", {\n        nameServer: { ipAddr: \"1.1.1.1\", port: 53 },\n      });\n      console.log(`  Deno.resolveDns(${domain}) => ${JSON.stringify(records)}`);\n      if (records.length === 0) {\n        throw new Error(\"Expected at least one CNAME record\");\n      }\n    } catch (err) {\n      console.log(`  Deno.resolveDns(${domain}) => ERROR: ${(err as Error).message}`);\n      // Some test domains may not have CNAME — that's ok, we still compare behavior\n    }\n  });\n}\n\n// ─── Tests: DoH (fetch-based) ───\n\nfor (const domain of TEST_DOMAINS) {\n  Deno.test(`[DoH/fetch] resolve CNAME for ${domain}`, async () => {\n    try {\n      const records = await resolveCnameDoH(domain, DOH_SERVERS[0]);\n      console.log(`  DoH(${domain}) => ${JSON.stringify(records)}`);\n      if (records.length === 0) {\n        throw new Error(\"Expected at least one CNAME record\");\n      }\n    } catch (err) {\n      console.log(`  DoH(${domain}) => ERROR: ${(err as Error).message}`);\n    }\n  });\n}\n\n// ─── Tests: Both return same results ───\n\nfor (const domain of TEST_DOMAINS) {\n  Deno.test(`[compare] Deno.resolveDns vs DoH return same result for ${domain}`, async () => {\n    let denoResult: string[] | null = null;\n    let dohResult: string[] | null = null;\n    let denoError: string | null = null;\n    let dohError: string | null = null;\n\n    try {\n      denoResult = await Deno.resolveDns(domain, \"CNAME\", {\n        nameServer: { ipAddr: \"1.1.1.1\", port: 53 },\n      });\n    } catch (err) {\n      denoError = (err as Error).message;\n    }\n\n    try {\n      dohResult = await resolveCnameDoH(domain, DOH_SERVERS[0]);\n    } catch (err) {\n      dohError = (err as Error).message;\n    }\n\n    console.log(`  Deno: ${denoResult ? JSON.stringify(denoResult) : `ERROR(${denoError})`}`);\n    console.log(`  DoH:  ${dohResult ? JSON.stringify(dohResult) : `ERROR(${dohError})`}`);\n\n    if (denoResult && dohResult) {\n      // Normalize trailing dots for comparison\n      const normalize = (r: string[]) => r.map((s) => s.replace(/\\.$/, \"\")).sort();\n      const d = normalize(denoResult);\n      const f = normalize(dohResult);\n\n      if (JSON.stringify(d) !== JSON.stringify(f)) {\n        throw new Error(\n          `Results differ!\\n  Deno: ${JSON.stringify(d)}\\n  DoH:  ${JSON.stringify(f)}`,\n        );\n      }\n      console.log(\"  ✓ Results match\");\n    } else if (denoResult === null && dohResult === null) {\n      console.log(\"  ✓ Both errored (consistent behavior)\");\n    } else {\n      console.log(\"  ⚠ One succeeded, other failed — check DNS config\");\n    }\n  });\n}\n\n// ─── Memory comparison: burst of resolutions ───\n\nDeno.test(\"[memory] Deno.resolveDns burst — check RSS delta\", async () => {\n  const before = Deno.memoryUsage();\n  const iterations = 50;\n\n  for (let i = 0; i < iterations; i++) {\n    for (const domain of TEST_DOMAINS) {\n      try {\n        await Deno.resolveDns(domain, \"CNAME\", {\n          nameServer: { ipAddr: \"1.1.1.1\", port: 53 },\n        });\n      } catch { /* ignore */ }\n    }\n  }\n\n  // deno-lint-ignore no-explicit-any\n  if (typeof (globalThis as any).gc === \"function\") (globalThis as any).gc();\n\n  const after = Deno.memoryUsage();\n  const rssDelta = after.rss - before.rss;\n  const heapDelta = after.heapUsed - before.heapUsed;\n\n  console.log(`  Deno.resolveDns (${iterations * TEST_DOMAINS.length} calls):`);\n  console.log(`    RSS before:  ${(before.rss / 1024 / 1024).toFixed(2)}MB`);\n  console.log(`    RSS after:   ${(after.rss / 1024 / 1024).toFixed(2)}MB`);\n  console.log(`    RSS delta:   ${(rssDelta / 1024 / 1024).toFixed(2)}MB`);\n  console.log(`    Heap delta:  ${(heapDelta / 1024 / 1024).toFixed(2)}MB`);\n});\n\nDeno.test(\"[memory] DoH/fetch burst — check RSS delta\", async () => {\n  const before = Deno.memoryUsage();\n  const iterations = 50;\n\n  for (let i = 0; i < iterations; i++) {\n    for (const domain of TEST_DOMAINS) {\n      try {\n        await resolveCnameDoH(domain, DOH_SERVERS[0]);\n      } catch { /* ignore */ }\n    }\n  }\n\n  // deno-lint-ignore no-explicit-any\n  if (typeof (globalThis as any).gc === \"function\") (globalThis as any).gc();\n\n  const after = Deno.memoryUsage();\n  const rssDelta = after.rss - before.rss;\n  const heapDelta = after.heapUsed - before.heapUsed;\n\n  console.log(`  DoH/fetch (${iterations * TEST_DOMAINS.length} calls):`);\n  console.log(`    RSS before:  ${(before.rss / 1024 / 1024).toFixed(2)}MB`);\n  console.log(`    RSS after:   ${(after.rss / 1024 / 1024).toFixed(2)}MB`);\n  console.log(`    RSS delta:   ${(rssDelta / 1024 / 1024).toFixed(2)}MB`);\n  console.log(`    Heap delta:  ${(heapDelta / 1024 / 1024).toFixed(2)}MB`);\n});\n"
  },
  {
    "path": "src/helpers/logger.ts",
    "content": "import { config } from \"../config.ts\";\n\nconst LEVELS: Record<string, number> = {\n  debug: 0,\n  info: 1,\n  warn: 2,\n  error: 3,\n};\n\nconst currentLevel = LEVELS[config.loggerLevel] ?? 0;\n\nexport const logger = {\n  debug: (...args: unknown[]) => {\n    if (currentLevel <= LEVELS.debug) console.debug(...args);\n  },\n  info: (...args: unknown[]) => {\n    if (currentLevel <= LEVELS.info) console.info(...args);\n  },\n  warn: (...args: unknown[]) => {\n    if (currentLevel <= LEVELS.warn) console.warn(...args);\n  },\n  error: (...args: unknown[]) => {\n    if (currentLevel <= LEVELS.error) console.error(...args);\n  },\n  log: (...args: unknown[]) => {\n    console.log(...args);\n  },\n};\n"
  },
  {
    "path": "src/main.ts",
    "content": "import { Hono } from \"hono\";\nimport vento from \"ventojs\";\nimport { config } from \"./config.ts\";\nimport { errorHandler } from \"./middleware/error-handler.ts\";\nimport { guardian } from \"./services/guardian.ts\";\nimport {\n  HttpError,\n  resolveDnsAndRedirect,\n} from \"./services/redirect.ts\";\nimport { dnsCacheSize, dnsInflightSize } from \"./helpers/dns.ts\";\n\nconst app = new Hono();\n\n// Pre-render homepage at startup (raw + gzip) to avoid per-request\n// template execution and CompressionStream allocations.\nconst homepage = await (async () => {\n  const env = vento({\n    includes: new URL(\"../views\", import.meta.url).pathname,\n    autoescape: false,\n  });\n  const template = await env.load(\"index.vto\");\n  const result = await template({ app: config });\n  const html = result.content;\n\n  const htmlBytes = new TextEncoder().encode(html);\n  const gzipStream = new CompressionStream(\"gzip\");\n  const compressed = await new Response(\n    new Blob([htmlBytes]).stream().pipeThrough(gzipStream),\n  ).arrayBuffer();\n\n  return { html, gzip: new Uint8Array(compressed) };\n})();\n\napp.onError(errorHandler);\n\n// Access log middleware\napp.use(\"/\", async (c, next) => {\n  // remoteAddr = real TCP connection IP (can't be spoofed)\n  // x-forwarded-for/x-real-ip are only trustworthy behind a reverse proxy\n  const ip = ((c.env as Record<string, unknown>)?.remoteAddr as Deno.NetAddr | undefined)?.hostname ||\n    c.req.header(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ||\n    c.req.header(\"x-real-ip\") ||\n    \"-\";\n  const host = c.req.header(\"host\") || \"-\";\n  const method = c.req.method;\n  const url = new URL(c.req.url);\n  const path = url.pathname + url.search;\n  const ua = c.req.header(\"user-agent\") || \"-\";\n\n  // Log BEFORE processing\n  console.log(\n    `[req] ${ip} \"${method} ${path}\" host=${host} ua=\"${ua}\"`,\n  );\n\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n\n  // Log AFTER processing\n  const status = c.res.status;\n  const location = c.res.headers.get(\"location\") || \"-\";\n\n  console.log(\n    `[res] ${ip} \"${method} ${path}\" host=${host} ${status} location=${location} ${ms}ms`,\n  );\n});\n\n// Homepage - only for the FQDN host (served from pre-rendered cache)\napp.get(\"/\", async (c, next) => {\n  const host = (c.req.header(\"host\") || \"\").split(\":\")[0];\n\n  if (host === config.fqdn) {\n    const ua = c.req.header(\"user-agent\");\n    if (!ua) return c.json({ statusCode: 403, message: \"Forbidden\" }, 403);\n\n    const acceptsGzip = c.req.header(\"accept-encoding\")?.includes(\"gzip\") ?? false;\n    const headers: Record<string, string> = {\n      \"Content-Type\": \"text/html; charset=utf-8\",\n      \"Cache-Control\": \"public, max-age=300\",\n    };\n    if (acceptsGzip) headers[\"Content-Encoding\"] = \"gzip\";\n\n    return new Response(acceptsGzip ? homepage.gzip : homepage.html, { headers });\n  }\n\n  // If not FQDN, skip to redirect\n  await next();\n});\n\n// Diagnostic endpoint — only accessible on the FQDN\napp.get(\"/healthz\", (c) => {\n  const host = (c.req.header(\"host\") || \"\").split(\":\")[0];\n  if (host !== config.fqdn) return c.notFound();\n\n  const mem = Deno.memoryUsage();\n  return c.json({\n    uptime: Math.floor(performance.now() / 1000),\n    memory: {\n      rss: `${(mem.rss / 1024 / 1024).toFixed(1)}MB`,\n      heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB`,\n      heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(1)}MB`,\n      external: `${(mem.external / 1024 / 1024).toFixed(1)}MB`,\n    },\n    dnsCache: dnsCacheSize(),\n    dnsInflight: dnsInflightSize(),\n  });\n});\n\n// robots.txt for redirect domains — tells crawlers not to follow/index redirects\napp.get(\"/robots.txt\", (c) => {\n  const host = (c.req.header(\"host\") || \"\").split(\":\")[0];\n  if (host === config.fqdn) return c.notFound();\n  c.header(\"Cache-Control\", \"public, max-age=86400\");\n  return c.text(\"User-agent: *\\nDisallow: /\\n\");\n});\n\n// FQDN-only routes: return 404 for non-redirect paths on the service domain\napp.all(\"/*\", async (c, next) => {\n  const host = (c.req.header(\"host\") || \"\").split(\":\")[0];\n  if (host === config.fqdn) {\n    return c.json({ statusCode: 404, message: \"Not Found\" }, 404);\n  }\n  await next();\n});\n\n// All other routes - redirect logic\napp.all(\"/*\", handleRedirect);\n\nasync function handleRedirect(c: import(\"hono\").Context): Promise<Response> {\n  let host = c.req.header(\"host\") || \"\";\n  if (!host) throw new HttpError(400, \"Bad Request\");\n  host = host.includes(\":\") ? host.split(\":\")[0] : host;\n\n  // Block requests without User-Agent (bots that follow redirects infinitely)\n  if (!c.req.header(\"user-agent\")) {\n    throw new HttpError(403, \"Forbidden\");\n  }\n\n  // Source guardian check\n  if (guardian.isDenied(host)) {\n    throw new HttpError(403, \"Forbidden\");\n  }\n\n  // Resolve redirect\n  const redirect = await resolveDnsAndRedirect(host, c.req.url.replace(/^https?:\\/\\/[^/]+/, \"\"));\n\n  // Destination guardian check\n  if (guardian.isDenied(redirect.fqdn)) {\n    throw new HttpError(403, \"Forbidden\");\n  }\n\n  // Self-redirect loop detection: destination points back to the same host\n  if (redirect.fqdn === host) {\n    throw new HttpError(508, `Loop detected: ${host} redirects to itself`);\n  }\n\n  // Encode non-ASCII characters to avoid ByteString errors in Response headers\n  let safeLocation: string;\n  try {\n    safeLocation = new URL(redirect.url).href;\n  } catch {\n    safeLocation = encodeURI(redirect.url);\n  }\n\n  // Use \" \" instead of null to work around Deno.serve memory leak\n  // See: https://github.com/denoland/deno/issues/27545\n  return new Response(\" \", {\n    status: redirect.status,\n    headers: {\n      \"Location\": safeLocation,\n      \"Cache-Control\": \"public, max-age=15\",\n    },\n  });\n}\n\n// Periodic health log — helps correlate CPU spikes in CloudWatch with memory/cache state\n// Memory watchdog — graceful restart when RSS exceeds limit (Deno native memory leak workaround)\n// See: https://github.com/denoland/deno/issues/28307\nconst RSS_LIMIT = Number(Deno.env.get(\"RSS_LIMIT_MB\") || \"384\") * 1024 * 1024;\n\nsetInterval(() => {\n  const mem = Deno.memoryUsage();\n  console.log(\n    `[health] rss=${(mem.rss / 1024 / 1024).toFixed(1)}MB heap=${(mem.heapUsed / 1024 / 1024).toFixed(1)}/${(mem.heapTotal / 1024 / 1024).toFixed(1)}MB external=${(mem.external / 1024 / 1024).toFixed(1)}MB dnsCache=${dnsCacheSize()} dnsInflight=${dnsInflightSize()}`,\n  );\n\n  if (mem.rss > RSS_LIMIT) {\n    console.warn(`[watchdog] RSS ${(mem.rss / 1024 / 1024).toFixed(0)}MB exceeded limit ${(RSS_LIMIT / 1024 / 1024).toFixed(0)}MB, restarting...`);\n    Deno.exit(0);\n  }\n}, 60_000);\n\n// Start server\nDeno.serve(\n  {\n    port: config.listenPort,\n    hostname: config.listenIp,\n    onListen({ hostname, port }) {\n      console.log(`[server] Server is listening on ${hostname}:${port}`);\n    },\n    onError(error) {\n      console.error(`[server] ${error}`);\n      return new Response(\"Internal Server Error\", { status: 500 });\n    },\n  },\n  app.fetch,\n);\n"
  },
  {
    "path": "src/middleware/error-handler.ts",
    "content": "import type { ErrorHandler } from \"hono\";\nimport { HttpError } from \"../services/redirect.ts\";\nimport { logger } from \"../helpers/logger.ts\";\n\nexport const errorHandler: ErrorHandler = (err, c) => {\n  const status = err instanceof HttpError ? err.status : 500;\n  const message = err.message || \"Internal Server Error\";\n\n  if (err instanceof HttpError) {\n    // Known/expected errors — log without stack trace\n    if (status >= 500) {\n      logger.warn(`[error] ${status} ${message}`);\n    }\n  } else {\n    // Unexpected errors — log full stack for investigation\n    logger.error(\n      `[error] investigate this error: ${err.name}/${err.message}`,\n      err.stack,\n    );\n  }\n\n  return c.json({ statusCode: status, message }, status as 400);\n};\n"
  },
  {
    "path": "src/services/guardian.ts",
    "content": "import psl from \"psl\";\nimport { logger } from \"../helpers/logger.ts\";\n\ninterface GuardianData {\n  denyFqdn: string[];\n}\n\nclass GuardianService {\n  private filepath: string;\n  private denySet = new Set<string>();\n\n  constructor() {\n    this.filepath = new URL(\"../../db/guardian.json\", import.meta.url).pathname;\n    this.openAndParse();\n\n    const interval = 60 * 1000;\n    setInterval(() => {\n      logger.debug(`[guardian] db.reload - interval ${interval}`);\n      this.openAndParse();\n    }, interval);\n  }\n\n  isDenied(fqdn: string): boolean {\n    // O(1) check against FQDN\n    if (this.denySet.has(fqdn)) return true;\n\n    // Extract base domain with simple split (covers most cases: example.com from sub.example.com)\n    const parts = fqdn.split(\".\");\n    if (parts.length > 2) {\n      const baseDomain = parts.slice(-2).join(\".\");\n      if (this.denySet.has(baseDomain)) return true;\n    }\n\n    return false;\n  }\n\n  openAndParse(): void {\n    try {\n      const text = Deno.readTextFileSync(this.filepath);\n      const data: GuardianData = JSON.parse(text || \"{}\");\n\n      // Pre-compute: add both raw entries and their psl-parsed base domains\n      const newSet = new Set<string>();\n      for (const fqdn of data.denyFqdn ?? []) {\n        newSet.add(fqdn);\n        const parsed = psl.parse(fqdn);\n        if (\"domain\" in parsed && parsed.domain) {\n          newSet.add(parsed.domain);\n        }\n      }\n      this.denySet = newSet;\n    } catch (err) {\n      logger.error(`[guardian] Failed to load guardian.json: ${err}`);\n    }\n  }\n}\n\nexport const guardian = new GuardianService();\n"
  },
  {
    "path": "src/services/redirect.ts",
    "content": "import { config } from \"../config.ts\";\nimport { createDestination } from \"../types/destination.ts\";\nimport type { Destination } from \"../types/destination.ts\";\nimport { RedirectResponse } from \"../types/redirect-response.ts\";\nimport { dnsResolveCname } from \"../helpers/dns.ts\";\nimport { decode } from \"../helpers/base32.ts\";\nimport { logger } from \"../helpers/logger.ts\";\n\n// Reusable TextDecoder — avoids creating native objects per request\nconst textDecoder = new TextDecoder();\n\nexport class HttpError extends Error {\n  constructor(public status: number, message: string) {\n    super(message);\n    this.name = \"HttpError\";\n  }\n}\n\nexport async function resolveDnsAndRedirect(\n  host: string,\n  reqUrl: string,\n): Promise<RedirectResponse> {\n  const raw = await resolveDns(host);\n  return getRedirectResponse(raw, reqUrl);\n}\n\nexport function getRedirectResponse(\n  raw: string,\n  reqUrl: string,\n): RedirectResponse {\n  const destination = parseDestination(raw, reqUrl);\n  return new RedirectResponse(destination);\n}\n\nexport async function resolveDns(host: string): Promise<string> {\n  // Extract subdomains via simple split (avoids heavy parseDomain trie lookup)\n  const labels = host.split(\".\");\n  const hasRedirectSubdomain = labels.includes(\"redirect\");\n\n  try {\n    const resolved = await dnsResolveCname(host);\n\n    if (resolved.length > 1) {\n      throw new HttpError(400, `More than one record on the host ${host}`);\n    }\n\n    // Remove trailing dot from CNAME if present\n    return resolved[0].replace(/\\.$/, \"\");\n  } catch (err: unknown) {\n    const error = err as { code?: string; name?: string; status?: number; message?: string };\n\n    // Deno's DNS resolver may throw errors without a `code` property\n    // (e.g., \"no records found for Query ...\"). Detect and normalize them.\n    const isDnsNotFound = error.code === \"ENODATA\" ||\n      (!error.code && error.message?.includes(\"no records found\"));\n\n    if (\n      isDnsNotFound &&\n      !hasRedirectSubdomain\n    ) {\n      return resolveDns(`redirect.${host}`);\n    }\n\n    const isKnownDnsError = isDnsNotFound ||\n      [\"ENOTFOUND\", \"ESERVFAIL\", \"EBADRESP\", \"ECONNREFUSED\"].includes(\n        error.code ?? \"\",\n      ) ||\n      error.message?.includes(\"invalid characters\") ||\n      error.message?.includes(\"proto error\");\n\n    if (isKnownDnsError) {\n      throw new HttpError(\n        400,\n        `The destination is not properly set, check the host ${host}`,\n      );\n    }\n\n    throw err;\n  }\n}\n\nexport function parseDestination(raw: string, reqUrl: string): Destination {\n  const destination = createDestination();\n\n  let parsedUrl: URL;\n  try {\n    parsedUrl = new URL(reqUrl, \"http://placeholder\");\n  } catch {\n    throw new HttpError(400, \"Bad Request\");\n  }\n\n  // Remove trailing dot and FQDN suffix\n  raw = raw.replace(/\\.$/, \"\");\n  raw = raw.replace(`.${config.fqdn}`, \"\");\n\n  let r: RegExpMatchArray | null;\n\n  let labels = raw.split(\".\");\n\n  labels = labels.map((label) => {\n    switch (true) {\n      case !!label.match(/^(opts-|_)https$/): {\n        destination.protocol = \"https\";\n        return \"\";\n      }\n      case !!(r = label.match(/^(?:opts-|_)(?:path)-(.*)$/)): {\n        r![1] = r![1].replace(/-/g, \"=\");\n        destination.pathnames.push(\n          textDecoder.decode(decode(r![1])),\n        );\n        return \"\";\n      }\n      case !!(r = label.match(/^(?:opts-|_)statuscode-(301|302|307|308)$/)): {\n        destination.status = parseInt(r![1]);\n        return \"\";\n      }\n      case !!(r = label.match(/^(?:opts-|_)port-(\\d+)$/)): {\n        destination.port = parseInt(r![1]);\n        return \"\";\n      }\n      case !!label.match(/^(opts-|_)uri$/): {\n        if (parsedUrl.search) {\n          destination.queries.push(parsedUrl.search.substring(1));\n        }\n        if (parsedUrl.pathname && parsedUrl.pathname !== \"/\") {\n          destination.pathnames.push(parsedUrl.pathname);\n        }\n        return \"\";\n      }\n      default:\n        return label;\n    }\n  });\n\n  raw = labels.filter((e) => e).join(\".\");\n\n  /* opts-query */\n  {\n    const queries: string[] = [];\n    let loop = 1;\n\n    while (\n      (r = raw.match(/\\.(?:opts-|_|)(?:query|base32)[.\\-]([^.]+)/))\n    ) {\n      if (loop++ > 5) logger.warn(`[redirect] CHECK RAW (query) ${raw}`);\n\n      raw = raw.replace(r[0], \"\");\n      r[1] = r[1].replace(/-/g, \"=\");\n      queries.push(textDecoder.decode(decode(r[1])));\n    }\n\n    destination.queries = [...queries, ...destination.queries];\n  }\n\n  /* opts-slash */\n  {\n    const pathnames: string[] = [];\n    let loop = 1;\n\n    while (\n      (r = raw.match(\n        /(\\.(?:opts-|_|)slash\\.)(.*?)(?:(?:(?:.opts-slash|.slash|_slash))|$)/,\n      )) ||\n      (r = raw.match(/\\.(?:opts-|_|)slash/))\n    ) {\n      if (loop++ > 5) logger.warn(`[redirect] CHECK RAW (slash) ${raw}`);\n\n      if (r && r[2]) {\n        raw = raw.replace(`${r[1]}${r[2]}`, \"\");\n        pathnames.push(`/${r[2]}`);\n      } else {\n        raw = raw.replace(r![0], \"\");\n        pathnames.push(\"/\");\n      }\n    }\n\n    destination.pathnames = [...pathnames, ...destination.pathnames];\n  }\n\n  destination.host = raw;\n\n  return destination;\n}\n"
  },
  {
    "path": "src/services/redirect_test.ts",
    "content": "import { assertEquals } from \"https://deno.land/std@0.224.0/assert/mod.ts\";\nimport { parseDestination } from \"./redirect.ts\";\n\n// Override config.fqdn for tests\nimport { config } from \"../config.ts\";\n(config as { fqdn: string }).fqdn = \"redirect.center\";\n\nDeno.test(\"parseDestination - opts-slash 1\", () => {\n  const raw = \"www.youtube.com.opts-slash.watch.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/watch\"],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - opts-slash 2\", () => {\n  const raw = \"www.youtube.com.opts-slash.watch.opts-slash.abc.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/watch\", \"/abc\"],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - opts-slash 3\", () => {\n  const raw = \"www.youtube.com.opts-slash.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/\"],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - opts-slash 4\", () => {\n  const raw = \"www.youtube.com.opts-slash.watch.opts-slash.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/watch\", \"/\"],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - slash 1\", () => {\n  const raw = \"www.youtube.com.slash.watch.slash.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/watch\", \"/\"],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - opts-https 1\", () => {\n  const raw = \"www.youtube.com.opts-https.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"https\",\n    pathnames: [],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - opts-statuscode 1\", () => {\n  const raw = \"www.youtube.com.opts-statuscode-302.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [],\n    status: 302,\n    host: \"www.youtube.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - opts-uri 1\", () => {\n  const raw = \"www.youtube.com.opts-uri.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/any\"],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [\"any=true\"],\n  });\n});\n\nDeno.test(\"parseDestination - opts-query 1\", () => {\n  const raw = \"www.youtube.com.opts-query-IFXFS===.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [\"AnY\"],\n  });\n});\n\nDeno.test(\"parseDestination - opts-query 2\", () => {\n  const raw = \"www.youtube.com.opts-query-IFXFS---.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [\"AnY\"],\n  });\n});\n\nDeno.test(\"parseDestination - opts-port\", () => {\n  const raw = \"www.youtube.com.opts-port-8080.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [],\n    status: 301,\n    host: \"www.youtube.com\",\n    queries: [],\n    port: 8080,\n  });\n});\n\nDeno.test(\"parseDestination - mix 1\", () => {\n  const raw = \"127.0.0.1.opts-slash.opts-query.ifqueysdmm.opts-https.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"https\",\n    pathnames: [\"/\"],\n    status: 301,\n    host: \"127.0.0.1\",\n    queries: [\"AaBbCc\"],\n  });\n});\n\nDeno.test(\"parseDestination - mix 2\", () => {\n  const raw = \"127.0.0.1.opts-path-ifqueysdmm.opts-https.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"https\",\n    pathnames: [\"AaBbCc\"],\n    status: 301,\n    host: \"127.0.0.1\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - mix 3\", () => {\n  const raw = \"www.test.com.opts-slash.xmart.opts-slash.xmart.dll.opts-https.redirect.center\";\n  const response = parseDestination(raw, \"/any?any=true\");\n  assertEquals(response, {\n    protocol: \"https\",\n    pathnames: [\"/xmart\", \"/xmart.dll\"],\n    status: 301,\n    host: \"www.test.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - mix 4\", () => {\n  const raw = \"www.google.com.opts-path-f52gk43u.opts-query-mfrggplemvta.opts-https.redirect.center\";\n  const response = parseDestination(raw, \"/\");\n  assertEquals(response, {\n    protocol: \"https\",\n    pathnames: [\"/test\"],\n    status: 301,\n    host: \"www.google.com\",\n    queries: [\"abc=def\"],\n  });\n});\n\nDeno.test(\"parseDestination - mix 5\", () => {\n  const raw = \"www.google.com.opts-path-f52gk43u.opts-query-mfrggplemvta.opts-https.opts-uri.redirect.center\";\n  const response = parseDestination(raw, \"/abc?fxa\");\n  assertEquals(response, {\n    protocol: \"https\",\n    pathnames: [\"/test\", \"/abc\"],\n    status: 301,\n    host: \"www.google.com\",\n    queries: [\"abc=def\", \"fxa\"],\n  });\n});\n\nDeno.test(\"parseDestination - mix 6\", () => {\n  const raw = \"www.google.com.opts-slash.test.opts-slash.abc.html.redirect.center.\";\n  const response = parseDestination(raw, \"/\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/test\", \"/abc.html\"],\n    status: 301,\n    host: \"www.google.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - mix 7\", () => {\n  const raw = \"www.google.com.opts-slash.test.opts-slash.abc.opts-slash.redirect.center.\";\n  const response = parseDestination(raw, \"/\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/test\", \"/abc\", \"/\"],\n    status: 301,\n    host: \"www.google.com\",\n    queries: [],\n  });\n});\n\nDeno.test(\"parseDestination - mix 8\", () => {\n  const raw = \"127.0.0.1.opts-port-22602.opts-slash.test.redirect.center.\";\n  const response = parseDestination(raw, \"/\");\n  assertEquals(response, {\n    protocol: \"http\",\n    pathnames: [\"/test\"],\n    status: 301,\n    port: 22602,\n    host: \"127.0.0.1\",\n    queries: [],\n  });\n});\n"
  },
  {
    "path": "src/services/statistic.ts",
    "content": "import { parseDomain } from \"parse-domain\";\nimport { logger } from \"../helpers/logger.ts\";\n\ninterface Statistic {\n  count: number;\n  firstTime?: string;\n  lastTime?: string;\n}\n\ninterface StatisticOverview {\n  periodDomains: number;\n  everDomains: number;\n}\n\nclass StatisticService {\n  private kv!: Deno.Kv;\n  private ready: Promise<void>;\n\n  constructor() {\n    this.ready = this.init();\n  }\n\n  private async init(): Promise<void> {\n    this.kv = await Deno.openKv();\n    await this.kv.set([\"meta\", \"started\"], new Date().toISOString());\n  }\n\n  async ensureReady(): Promise<void> {\n    await this.ready;\n  }\n\n  async write(host: string): Promise<void> {\n    await this.ensureReady();\n    logger.debug(`[statistic] write received host ${host}`);\n\n    const parsedHost = parseDomain(host) as {\n      domain: string;\n      topLevelDomains: string[];\n    };\n\n    const domain =\n      `${parsedHost.domain}.${parsedHost.topLevelDomains.join(\".\")}`.toLowerCase();\n    await this.entryDomain(domain);\n  }\n\n  private async entryDomain(domain: string): Promise<void> {\n    const key = [\"domain\", domain];\n    const existing = await this.kv.get<Statistic>(key);\n\n    const entry: Statistic = existing.value ?? {\n      count: 0,\n      firstTime: new Date().toISOString(),\n    };\n\n    entry.count += 1;\n    entry.lastTime = new Date().toISOString();\n\n    await this.kv.set(key, entry);\n    logger.debug(\n      `[statistic] entryDomain key ${domain}, entry: ${JSON.stringify(entry)}`,\n    );\n  }\n\n  async overview(): Promise<StatisticOverview> {\n    await this.ensureReady();\n\n    const dayBefore = new Date();\n    dayBefore.setDate(dayBefore.getDate() - 1);\n    const dayBeforeISO = dayBefore.toISOString();\n\n    let periodDomains = 0;\n    let everDomains = 0;\n\n    const iter = this.kv.list<Statistic>({ prefix: [\"domain\"] });\n    for await (const entry of iter) {\n      everDomains++;\n      if (entry.value.lastTime && entry.value.lastTime >= dayBeforeISO) {\n        periodDomains++;\n      }\n    }\n\n    return { periodDomains, everDomains };\n  }\n}\n\nexport const statistic = new StatisticService();\n"
  },
  {
    "path": "src/types/destination.ts",
    "content": "export interface Destination {\n  protocol: \"http\" | \"https\";\n  host: string;\n  pathnames: string[];\n  queries: string[];\n  status: number;\n  port?: number;\n}\n\nexport function createDestination(): Destination {\n  return {\n    protocol: \"http\",\n    host: \"\",\n    pathnames: [],\n    queries: [],\n    status: 301,\n  };\n}\n"
  },
  {
    "path": "src/types/redirect-response.ts",
    "content": "import { Destination } from \"./destination.ts\";\n\nexport class RedirectResponse {\n  url: string;\n  status: number;\n  fqdn: string;\n\n  constructor(destination: Destination) {\n    this.fqdn = destination.host;\n    this.status = destination.status;\n    this.url = `${destination.protocol}://${destination.host}`;\n\n    if (destination.port && destination.port > 0 && destination.port <= 65535) {\n      this.url += `:${destination.port}`;\n    }\n\n    this.url += \"/\";\n\n    if (destination.pathnames.length) {\n      const path = destination.pathnames.join(\"\");\n      if (path.startsWith(\"/\")) this.url += path.substring(1);\n      else this.url += path;\n    }\n\n    if (destination.queries.length >= 1) {\n      this.url += `?${destination.queries.join(\"&\")}`.replace(\"?#\", \"#\");\n    }\n  }\n}\n"
  },
  {
    "path": "views/index.vto",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <title>{{ app.projectName }} - Free DNS-Based Domain Redirect Service</title>\n  <meta name=\"description\" content=\"Redirect any domain using only a DNS CNAME record. Free, open-source URL redirect service — no server, no hosting, no code. Supports HTTP 301, 302, 307, 308, HTTPS, path forwarding and query strings.\" />\n  <meta name=\"keywords\" content=\"dns redirect, cname redirect, domain redirect, url redirect, free redirect service, domain forwarding, dns url forwarding, cname record redirect, 301 redirect dns, redirecionamento dns, redirecionar dominio, redirecionamento de url\" />\n  <meta name=\"author\" content=\"Udlei Nati\" />\n  <meta name=\"robots\" content=\"index, follow\" />\n  <meta name=\"theme-color\" content=\"#2563eb\" />\n  <link rel=\"canonical\" href=\"https://{{ app.fqdn }}/\" />\n  <link rel=\"alternate\" hreflang=\"en\" href=\"https://{{ app.fqdn }}/\" />\n  <link rel=\"alternate\" hreflang=\"pt\" href=\"https://{{ app.fqdn }}/\" />\n  <link rel=\"alternate\" hreflang=\"x-default\" href=\"https://{{ app.fqdn }}/\" />\n\n  <!-- Open Graph -->\n  <meta property=\"og:title\" content=\"{{ app.projectName }} - Free DNS-Based Domain Redirect Service\" />\n  <meta property=\"og:description\" content=\"Redirect any domain using only a DNS CNAME record. Free, open-source — no server, no hosting, no code.\" />\n  <meta property=\"og:type\" content=\"website\" />\n  <meta property=\"og:url\" content=\"https://{{ app.fqdn }}/\" />\n  <meta property=\"og:site_name\" content=\"{{ app.projectName }}\" />\n  <meta property=\"og:locale\" content=\"en_US\" />\n  <meta property=\"og:locale:alternate\" content=\"pt_BR\" />\n\n  <!-- Twitter Card -->\n  <meta name=\"twitter:card\" content=\"summary\" />\n  <meta name=\"twitter:title\" content=\"{{ app.projectName }} - Free DNS-Based Domain Redirect Service\" />\n  <meta name=\"twitter:description\" content=\"Redirect any domain using only a DNS CNAME record. Free, open-source — no server, no hosting, no code.\" />\n\n  <!-- JSON-LD Structured Data -->\n  <script type=\"application/ld+json\">\n  {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebApplication\",\n    \"name\": \"{{ app.projectName }}\",\n    \"url\": \"https://{{ app.fqdn }}/\",\n    \"description\": \"Free DNS-based domain redirect service. Redirect URLs, domains, and subdomains using only CNAME records.\",\n    \"applicationCategory\": \"DeveloperApplication\",\n    \"operatingSystem\": \"Any\",\n    \"offers\": {\n      \"@type\": \"Offer\",\n      \"price\": \"0\",\n      \"priceCurrency\": \"USD\"\n    },\n    \"author\": {\n      \"@type\": \"Person\",\n      \"name\": \"Udlei Nati\"\n    },\n    \"license\": \"https://opensource.org/licenses/MIT\",\n    \"isAccessibleForFree\": true,\n    \"inLanguage\": [\"en\", \"pt\", \"es\", \"de\", \"fr\", \"it\", \"ja\", \"ru\", \"ko\", \"zh\", \"ar\", \"hi\"]\n  }\n  </script>\n\n  <link rel=\"icon\" href=\"data:,\" />\n  <script src=\"https://unpkg.com/base32.js@0.1.0/dist/base32.min.js\"></script>\n  <style>\n    :root {\n      --primary: #2563eb;\n      --primary-dark: #1d4ed8;\n      --bg: #f8fafc;\n      --surface: #ffffff;\n      --text: #1e293b;\n      --text-secondary: #64748b;\n      --border: #e2e8f0;\n      --code-bg: #f1f5f9;\n      --accent: #0ea5e9;\n      --success: #10b981;\n      --radius: 12px;\n    }\n\n    * { box-sizing: border-box; margin: 0; padding: 0; }\n\n    body {\n      font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n      background: var(--bg);\n      color: var(--text);\n      line-height: 1.6;\n    }\n\n    /* i18n: hide inactive lang */\n    body[data-lang=\"en\"] .pt { display: none !important; }\n    body[data-lang=\"pt\"] .en { display: none !important; }\n\n    .container {\n      max-width: 960px;\n      margin: 0 auto;\n      padding: 0 24px;\n    }\n\n    /* Lang switcher */\n    .lang-switch {\n      position: absolute;\n      top: 12px;\n      right: 24px;\n      display: flex;\n      gap: 6px;\n    }\n\n    .lang-switch button {\n      background: rgba(255,255,255,0.2);\n      border: 1px solid rgba(255,255,255,0.3);\n      color: #fff;\n      border-radius: 4px;\n      padding: 3px 10px;\n      font-size: 0.75rem;\n      font-weight: 600;\n      cursor: pointer;\n      transition: background 0.15s;\n    }\n\n    .lang-switch button:hover { background: rgba(255,255,255,0.3); }\n    .lang-switch button.active { background: rgba(255,255,255,0.95); color: var(--primary); }\n\n    /* Hero */\n    .hero {\n      background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);\n      color: #fff;\n      padding: 32px 0 28px;\n      text-align: center;\n      position: relative;\n    }\n\n    .hero h1 { font-size: 2rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 6px; }\n    .hero .tagline { font-size: 1rem; opacity: 0.92; margin-bottom: 16px; }\n\n    /* CTA Button */\n    .btn-cta {\n      display: inline-flex;\n      align-items: center;\n      gap: 8px;\n      background: linear-gradient(135deg, var(--primary), var(--accent));\n      color: #fff;\n      border: none;\n      border-radius: 10px;\n      padding: 14px 32px;\n      font-size: 1.05rem;\n      font-weight: 700;\n      cursor: pointer;\n      transition: transform 0.15s, box-shadow 0.15s;\n      box-shadow: 0 4px 16px rgba(37,99,235,0.3);\n      text-decoration: none;\n    }\n\n    .btn-cta:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(37,99,235,0.4); }\n    .btn-cta:active { transform: translateY(0); }\n    .btn-cta .icon { font-size: 1.2rem; }\n\n    /* Sections */\n    .section { padding: 40px 0; }\n    .section-title { font-size: 1.4rem; font-weight: 700; text-align: center; margin-bottom: 8px; }\n    .section-subtitle { text-align: center; color: var(--text-secondary); max-width: 640px; margin: 0 auto 28px; font-size: 0.92rem; }\n\n    /* Steps */\n    .steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n\n    .step {\n      background: var(--surface);\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      padding: 20px 16px;\n      text-align: center;\n      transition: box-shadow 0.2s;\n    }\n\n    .step:hover { box-shadow: 0 4px 24px rgba(0,0,0,0.06); }\n\n    .step-icon {\n      width: 40px; height: 40px; border-radius: 50%;\n      background: linear-gradient(135deg, var(--primary), var(--accent));\n      color: #fff; display: inline-flex; align-items: center; justify-content: center;\n      font-size: 1.1rem; font-weight: 700; margin-bottom: 10px;\n    }\n\n    .step h3 { font-size: 1rem; margin-bottom: 4px; }\n    .step p { font-size: 0.85rem; color: var(--text-secondary); }\n\n    /* Examples */\n    .examples { background: var(--surface); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }\n\n    .example-card {\n      background: var(--bg); border: 1px solid var(--border);\n      border-radius: var(--radius); margin-bottom: 10px; overflow: hidden;\n    }\n\n    .example-header {\n      padding: 12px 16px; cursor: pointer; display: flex; align-items: center;\n      gap: 10px; font-weight: 600; user-select: none; transition: background 0.15s;\n    }\n\n    .example-header:hover { background: var(--code-bg); }\n    .example-header .arrow { transition: transform 0.2s; font-size: 0.7rem; color: var(--text-secondary); flex-shrink: 0; }\n    .example-card.open .example-header .arrow { transform: rotate(90deg); }\n    .example-header .example-summary { flex: 1; font-size: 0.9rem; }\n    .example-header code { background: var(--code-bg); padding: 2px 6px; border-radius: 4px; font-size: 0.8rem; color: var(--primary); }\n    .example-body { display: none; padding: 16px 20px; border-top: 1px solid var(--border); background: #fff; }\n    .example-card.open .example-body { display: block; }\n\n    .dns-record {\n      background: var(--code-bg); border-radius: 8px; padding: 12px 16px;\n      font-family: \"SF Mono\", \"Fira Code\", \"Cascadia Code\", monospace;\n      font-size: 0.82rem; margin: 8px 0; overflow-x: auto;\n    }\n\n    .dns-record .row { display: flex; gap: 12px; padding: 3px 0; white-space: nowrap; }\n    .dns-record .label { color: var(--text-secondary); min-width: 80px; flex-shrink: 0; }\n    .dns-record .value { font-weight: 600; color: var(--text); }\n\n    .tip {\n      display: flex; align-items: flex-start; gap: 8px; margin-top: 10px;\n      padding: 8px 12px; background: #eff6ff; border-radius: 8px;\n      font-size: 0.84rem; color: var(--primary-dark);\n    }\n\n    .tip-icon { flex-shrink: 0; font-size: 0.9rem; }\n\n    /* Modal */\n    .modal-overlay {\n      display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);\n      backdrop-filter: blur(4px); z-index: 1000; align-items: center;\n      justify-content: center; padding: 24px;\n    }\n\n    .modal-overlay.open { display: flex; }\n\n    .modal {\n      background: var(--surface); border-radius: 16px; padding: 32px;\n      max-width: 560px; width: 100%; position: relative;\n      box-shadow: 0 24px 64px rgba(0,0,0,0.2); animation: modalIn 0.2s ease-out;\n    }\n\n    @keyframes modalIn {\n      from { opacity: 0; transform: scale(0.95) translateY(8px); }\n      to { opacity: 1; transform: scale(1) translateY(0); }\n    }\n\n    .modal-close { position: absolute; top: 12px; right: 16px; background: none; border: none; font-size: 1.5rem; color: var(--text-secondary); cursor: pointer; line-height: 1; padding: 4px; }\n    .modal-close:hover { color: var(--text); }\n    .modal h2 { font-size: 1.3rem; font-weight: 700; margin-bottom: 4px; }\n    .modal .modal-desc { color: var(--text-secondary); font-size: 0.88rem; margin-bottom: 20px; }\n    .modal label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 5px; color: var(--text); }\n    .modal input { width: 100%; padding: 11px 14px; border: 1px solid var(--border); border-radius: 8px; font-size: 0.92rem; font-family: inherit; outline: none; transition: border 0.2s; }\n    .modal input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }\n    .modal .converter-arrow { text-align: center; padding: 10px 0; color: var(--text-secondary); font-size: 1.2rem; }\n    .modal .input-group { position: relative; }\n    .modal .input-group input { padding-right: 80px; font-family: \"SF Mono\", \"Fira Code\", monospace; font-size: 0.82rem; }\n\n    .btn-copy { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); background: var(--primary); color: #fff; border: none; border-radius: 6px; padding: 7px 14px; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: background 0.15s; }\n    .btn-copy:hover { background: var(--primary-dark); }\n    .btn-copy.copied { background: var(--success); }\n\n    .modal-tip {\n      margin-top: 16px; padding: 10px 14px; background: #fef9c3;\n      border: 1px solid #fde047; border-radius: 8px; font-size: 0.82rem;\n      color: #854d0e; line-height: 1.5;\n    }\n\n    .modal-tip code { background: rgba(0,0,0,0.06); padding: 1px 5px; border-radius: 3px; font-size: 0.8rem; }\n\n    /* Parameters ref */\n    .params-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }\n    .params-table th { text-align: left; padding: 8px 12px; background: var(--code-bg); font-weight: 600; border-bottom: 2px solid var(--border); }\n    .params-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }\n    .params-table code { background: var(--code-bg); padding: 2px 5px; border-radius: 4px; font-size: 0.78rem; white-space: nowrap; }\n\n    /* Footer */\n    footer { background: var(--text); color: #cbd5e1; padding: 36px 0 24px; }\n    footer a { color: var(--accent); text-decoration: none; }\n    footer a:hover { text-decoration: underline; }\n    .footer-main { display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; margin-bottom: 24px; }\n    .footer-col { flex: 1; min-width: 200px; }\n    .footer-col h4 { color: #fff; margin-bottom: 8px; font-size: 0.95rem; }\n    .footer-col p, .footer-col li { font-size: 0.84rem; line-height: 1.6; }\n    .footer-col ul { list-style: none; }\n    .footer-seo { border-top: 1px solid rgba(255,255,255,0.08); padding-top: 20px; }\n    .footer-seo-block { margin-bottom: 12px; }\n    .footer-seo-block p { font-size: 0.72rem; color: #64748b; line-height: 1.5; }\n    .footer-bottom { border-top: 1px solid rgba(255,255,255,0.08); padding-top: 16px; margin-top: 16px; text-align: center; font-size: 0.78rem; color: #64748b; }\n\n    @media (max-width: 640px) {\n      .hero h1 { font-size: 1.6rem; }\n      .hero .tagline { font-size: 0.9rem; }\n      .steps { grid-template-columns: 1fr; }\n      .modal { padding: 24px 20px; margin: 16px; }\n      .dns-record .row { flex-direction: column; gap: 2px; }\n      .lang-switch { position: static; justify-content: center; margin-bottom: 12px; }\n    }\n  </style>\n</head>\n\n<body data-lang=\"en\">\n\n  <!-- Hero -->\n  <section class=\"hero\">\n    <div class=\"lang-switch\">\n      <button onclick=\"setLang('en')\" id=\"lang-en\" class=\"active\">EN</button>\n      <button onclick=\"setLang('pt')\" id=\"lang-pt\">PT</button>\n    </div>\n    <div class=\"container\">\n      <h1>{{ app.projectName }}</h1>\n      <p class=\"tagline\">\n        <span class=\"en\">Redirect any domain using only a DNS record. No server, no hosting, no code.</span>\n        <span class=\"pt\">Redirecione qualquer dom&iacute;nio usando apenas um registro DNS. Sem servidor, sem hospedagem, sem c&oacute;digo.</span>\n      </p>\n    </div>\n  </section>\n\n  <!-- How it works -->\n  <section class=\"section\">\n    <div class=\"container\">\n      <h2 class=\"section-title\">\n        <span class=\"en\">How it works</span>\n        <span class=\"pt\">Como funciona</span>\n      </h2>\n      <p class=\"section-subtitle\">\n        <span class=\"en\">Three steps. No account, no sign-up &mdash; completely free and open source.</span>\n        <span class=\"pt\">Tr&ecirc;s passos. Sem cadastro, sem conta &mdash; totalmente gratuito e open source.</span>\n      </p>\n      <div class=\"steps\">\n        <div class=\"step\">\n          <div class=\"step-icon\">1</div>\n          <h3>\n            <span class=\"en\">Point your domain</span>\n            <span class=\"pt\">Aponte seu dom&iacute;nio</span>\n          </h3>\n          <p>\n            <span class=\"en\">Create an <strong>A record</strong> pointing to <code>{{ app.entryIp }}</code></span>\n            <span class=\"pt\">Crie um <strong>registro A</strong> apontando para <code>{{ app.entryIp }}</code></span>\n          </p>\n        </div>\n        <div class=\"step\">\n          <div class=\"step-icon\">2</div>\n          <h3>\n            <span class=\"en\">Set the destination</span>\n            <span class=\"pt\">Defina o destino</span>\n          </h3>\n          <p>\n            <span class=\"en\">Create a <strong>CNAME</strong> like <code>dest.com.{{ app.fqdn }}</code></span>\n            <span class=\"pt\">Crie um <strong>CNAME</strong> como <code>dest.com.{{ app.fqdn }}</code></span>\n          </p>\n        </div>\n        <div class=\"step\">\n          <div class=\"step-icon\">3</div>\n          <h3>\n            <span class=\"en\">Done!</span>\n            <span class=\"pt\">Pronto!</span>\n          </h3>\n          <p>\n            <span class=\"en\">Visitors get a <strong>301 redirect</strong> automatically.</span>\n            <span class=\"pt\">Visitantes recebem um <strong>redirect 301</strong> automaticamente.</span>\n          </p>\n        </div>\n      </div>\n    </div>\n  </section>\n\n  <!-- How to use -->\n  <section class=\"section examples\">\n    <div class=\"container\">\n      <h2 class=\"section-title\">\n        <span class=\"en\">How to use</span>\n        <span class=\"pt\">Como usar</span>\n      </h2>\n      <p class=\"section-subtitle\">\n        <span class=\"en\">Click each example to see the DNS records you need.</span>\n        <span class=\"pt\">Clique em cada exemplo para ver os registros DNS necess&aacute;rios.</span>\n      </p>\n\n      <div class=\"example-card open\">\n        <div class=\"example-header\" onclick=\"toggleExample(this)\">\n          <span class=\"arrow\">&#9654;</span>\n          <span class=\"example-summary\">\n            <span class=\"en\">Redirect</span><span class=\"pt\">Redirecionar</span>\n            <code>my-domain.com</code> &rarr; <code>www.my-domain.com</code>\n          </span>\n        </div>\n        <div class=\"example-body\">\n          <p>\n            <span class=\"en\">Redirect your bare/root domain to the www version:</span>\n            <span class=\"pt\">Redirecione seu dom&iacute;nio raiz para a vers&atilde;o www:</span>\n          </p>\n          <div class=\"dns-record\">\n            <div class=\"row\">\n              <span class=\"label\">Host:</span>\n              <span class=\"value\">&lt;empty&gt; (or @)</span>\n              <span class=\"label\">Type:</span>\n              <span class=\"value\">A</span>\n              <span class=\"label\">Value:</span>\n              <span class=\"value\">{{ app.entryIp }}</span>\n            </div>\n            <div class=\"row\">\n              <span class=\"label\">Host:</span>\n              <span class=\"value\">redirect</span>\n              <span class=\"label\">Type:</span>\n              <span class=\"value\">CNAME</span>\n              <span class=\"label\">Value:</span>\n              <span class=\"value\">www.my-domain.com.{{ app.fqdn }}</span>\n            </div>\n          </div>\n          <div class=\"tip\">\n            <span class=\"tip-icon\">&#128161;</span>\n            <span>\n              <span class=\"en\">Add <code>.opts-https</code> to force HTTPS: <code>www.my-domain.com.opts-https.{{ app.fqdn }}</code></span>\n              <span class=\"pt\">Adicione <code>.opts-https</code> para for&ccedil;ar HTTPS: <code>www.my-domain.com.opts-https.{{ app.fqdn }}</code></span>\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"example-card\">\n        <div class=\"example-header\" onclick=\"toggleExample(this)\">\n          <span class=\"arrow\">&#9654;</span>\n          <span class=\"example-summary\">\n            <span class=\"en\">Redirect</span><span class=\"pt\">Redirecionar</span>\n            <code>www.my-domain.com</code> &rarr; <code>www.other-domain.com</code>\n          </span>\n        </div>\n        <div class=\"example-body\">\n          <p>\n            <span class=\"en\">Redirect a subdomain to a completely different domain:</span>\n            <span class=\"pt\">Redirecione um subdom&iacute;nio para um dom&iacute;nio completamente diferente:</span>\n          </p>\n          <div class=\"dns-record\">\n            <div class=\"row\">\n              <span class=\"label\">Host:</span>\n              <span class=\"value\">www</span>\n              <span class=\"label\">Type:</span>\n              <span class=\"value\">CNAME</span>\n              <span class=\"label\">Value:</span>\n              <span class=\"value\">www.other-domain.com.{{ app.fqdn }}</span>\n            </div>\n          </div>\n          <div class=\"tip\">\n            <span class=\"tip-icon\">&#128161;</span>\n            <span>\n              <span class=\"en\">Only one DNS record needed for subdomain redirects!</span>\n              <span class=\"pt\">Apenas um registro DNS necess&aacute;rio para redirecionamento de subdom&iacute;nios!</span>\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"example-card\">\n        <div class=\"example-header\" onclick=\"toggleExample(this)\">\n          <span class=\"arrow\">&#9654;</span>\n          <span class=\"example-summary\">\n            <span class=\"en\">Redirect preserving the path:</span>\n            <span class=\"pt\">Redirecionar preservando o caminho:</span>\n            <code>/page</code> &rarr; <code>/page</code>\n          </span>\n        </div>\n        <div class=\"example-body\">\n          <p>\n            <span class=\"en\">Keep the original URL path and query string when redirecting:</span>\n            <span class=\"pt\">Mantenha o caminho e a query string originais ao redirecionar:</span>\n          </p>\n          <div class=\"dns-record\">\n            <div class=\"row\">\n              <span class=\"label\">Host:</span>\n              <span class=\"value\">www</span>\n              <span class=\"label\">Type:</span>\n              <span class=\"value\">CNAME</span>\n              <span class=\"label\">Value:</span>\n              <span class=\"value\">www.other-domain.com.opts-uri.{{ app.fqdn }}</span>\n            </div>\n          </div>\n          <div class=\"tip\">\n            <span class=\"tip-icon\">&#128161;</span>\n            <span>\n              <span class=\"en\"><code>.opts-uri</code> passes the full request path and query string to the destination.</span>\n              <span class=\"pt\"><code>.opts-uri</code> repassa o caminho completo e a query string para o destino.</span>\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"example-card\">\n        <div class=\"example-header\" onclick=\"toggleExample(this)\">\n          <span class=\"arrow\">&#9654;</span>\n          <span class=\"example-summary\">\n            <span class=\"en\">Redirect with path &amp; query:</span>\n            <span class=\"pt\">Redirecionar com caminho &amp; query:</span>\n            <code>jobs.my-domain.com</code> &rarr; <code>my-domain.com/careers?ref=dns</code>\n          </span>\n        </div>\n        <div class=\"example-body\">\n          <p>\n            <span class=\"en\">Redirect a subdomain to a specific path with query parameters:</span>\n            <span class=\"pt\">Redirecione um subdom&iacute;nio para um caminho espec&iacute;fico com par&acirc;metros de consulta:</span>\n          </p>\n          <div class=\"dns-record\">\n            <div class=\"row\">\n              <span class=\"label\">Host:</span>\n              <span class=\"value\">jobs</span>\n              <span class=\"label\">Type:</span>\n              <span class=\"value\">CNAME</span>\n              <span class=\"label\">Value:</span>\n              <span class=\"value\">my-domain.com.opts-slash.careers.opts-query-onswg5lsnf2a.{{ app.fqdn }}</span>\n            </div>\n          </div>\n          <div class=\"tip\">\n            <span class=\"tip-icon\">&#128161;</span>\n            <span>\n              <span class=\"en\"><code>.opts-slash.careers</code> adds <code>/careers</code>. The query <code>ref=dns</code> is Base32-encoded. Use the <a href=\"javascript:;\" onclick=\"openModal()\" style=\"color:var(--primary-dark);font-weight:600\">CNAME Generator</a> to build these automatically.</span>\n              <span class=\"pt\"><code>.opts-slash.careers</code> adiciona <code>/careers</code>. A query <code>ref=dns</code> &eacute; codificada em Base32. Use o <a href=\"javascript:;\" onclick=\"openModal()\" style=\"color:var(--primary-dark);font-weight:600\">Gerador de CNAME</a> para gerar automaticamente.</span>\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <div style=\"text-align:center; margin-top: 24px;\">\n        <button class=\"btn-cta\" onclick=\"openModal()\">\n          <span class=\"icon\">&#9889;</span>\n          <span class=\"en\">CNAME Generator</span>\n          <span class=\"pt\">Gerador de CNAME</span>\n        </button>\n      </div>\n    </div>\n  </section>\n\n  <!-- Parameters Reference -->\n  <section class=\"section\">\n    <div class=\"container\">\n      <h2 class=\"section-title\">\n        <span class=\"en\">Parameters Reference</span>\n        <span class=\"pt\">Refer&ecirc;ncia de Par&acirc;metros</span>\n      </h2>\n      <p class=\"section-subtitle\">\n        <span class=\"en\">Modifiers you can add to your CNAME record to customize the redirect.</span>\n        <span class=\"pt\">Modificadores que voc&ecirc; pode adicionar ao seu registro CNAME para personalizar o redirecionamento.</span>\n      </p>\n      <table class=\"params-table\">\n        <thead>\n          <tr>\n            <th>\n              <span class=\"en\">Parameter</span>\n              <span class=\"pt\">Par&acirc;metro</span>\n            </th>\n            <th>\n              <span class=\"en\">Description</span>\n              <span class=\"pt\">Descri&ccedil;&atilde;o</span>\n            </th>\n            <th>\n              <span class=\"en\">Example</span>\n              <span class=\"pt\">Exemplo</span>\n            </th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td><code>.opts-https</code></td>\n            <td>\n              <span class=\"en\">Force redirect to HTTPS</span>\n              <span class=\"pt\">For&ccedil;ar redirecionamento para HTTPS</span>\n            </td>\n            <td><code>example.com.opts-https.{{ app.fqdn }}</code></td>\n          </tr>\n          <tr>\n            <td><code>.opts-uri</code></td>\n            <td>\n              <span class=\"en\">Preserve original path and query string</span>\n              <span class=\"pt\">Preservar caminho e query string originais</span>\n            </td>\n            <td><code>example.com.opts-uri.{{ app.fqdn }}</code></td>\n          </tr>\n          <tr>\n            <td><code>.opts-slash.{path}</code></td>\n            <td>\n              <span class=\"en\">Append a path to the destination URL</span>\n              <span class=\"pt\">Adicionar um caminho &agrave; URL de destino</span>\n            </td>\n            <td><code>example.com.opts-slash.blog.{{ app.fqdn }}</code></td>\n          </tr>\n          <tr>\n            <td><code>.opts-path-{base32}</code></td>\n            <td>\n              <span class=\"en\">Base32-encoded path (for special characters)</span>\n              <span class=\"pt\">Caminho codificado em Base32 (para caracteres especiais)</span>\n            </td>\n            <td><code>example.com.opts-path-mfrgg.{{ app.fqdn }}</code></td>\n          </tr>\n          <tr>\n            <td><code>.opts-query-{base32}</code></td>\n            <td>\n              <span class=\"en\">Base32-encoded query string</span>\n              <span class=\"pt\">Query string codificada em Base32</span>\n            </td>\n            <td><code>example.com.opts-query-nfxgg.{{ app.fqdn }}</code></td>\n          </tr>\n          <tr>\n            <td><code>.opts-statuscode-{code}</code></td>\n            <td>\n              <span class=\"en\">HTTP status code: 301, 302, 307 or 308</span>\n              <span class=\"pt\">C&oacute;digo de status HTTP: 301, 302, 307 ou 308</span>\n            </td>\n            <td><code>example.com.opts-statuscode-302.{{ app.fqdn }}</code></td>\n          </tr>\n          <tr>\n            <td><code>.opts-port-{port}</code></td>\n            <td>\n              <span class=\"en\">Redirect to a specific port</span>\n              <span class=\"pt\">Redirecionar para uma porta espec&iacute;fica</span>\n            </td>\n            <td><code>example.com.opts-port-8080.{{ app.fqdn }}</code></td>\n          </tr>\n        </tbody>\n      </table>\n    </div>\n  </section>\n\n  <!-- Footer -->\n  <footer>\n    <div class=\"container\">\n      <div class=\"footer-main\">\n        <div class=\"footer-col\">\n          <h4>{{ app.projectName }}</h4>\n          <p>\n            <span class=\"en\">Free, open-source DNS redirect service. No sign-up, no tracking, no limits.</span>\n            <span class=\"pt\">Servi&ccedil;o gratuito e open-source de redirecionamento via DNS. Sem cadastro, sem rastreamento, sem limites.</span>\n          </p>\n          <p style=\"margin-top: 8px;\">\n            <a href=\"https://github.com/udleinati/redirect.center\">GitHub</a> &middot;\n            <a href=\"https://github.com/udleinati/redirect.center/issues\">Issues</a> &middot;\n            <a href=\"mailto:udlei@nati.biz\">Contact</a>\n          </p>\n        </div>\n        <div class=\"footer-col\">\n          <h4>Quick Setup</h4>\n          <ul>\n            <li>1. A record &rarr; <code style=\"color:#94a3b8\">{{ app.entryIp }}</code></li>\n            <li>2. CNAME &rarr; <code style=\"color:#94a3b8\">dest.{{ app.fqdn }}</code></li>\n            <li>3. <span class=\"en\">Wait for DNS propagation</span><span class=\"pt\">Aguarde a propaga&ccedil;&atilde;o DNS</span></li>\n          </ul>\n        </div>\n      </div>\n\n      <div class=\"footer-seo\">\n        <div class=\"footer-seo-block\" lang=\"en\">\n          <p>redirect.center is a free DNS-based domain redirect service. Redirect URLs, domains, and subdomains using only CNAME records. No server configuration needed — just point your DNS and go. Supports HTTP 301, 302, 307, 308 redirects, path forwarding, query strings, HTTPS, and custom ports.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"pt\">\n          <p>redirect.center &eacute; um servi&ccedil;o gratuito de redirecionamento de dom&iacute;nios via DNS. Redirecione URLs, dom&iacute;nios e subdom&iacute;nios usando apenas registros CNAME. Sem necessidade de configurar servidor &mdash; basta apontar seu DNS. Suporta redirecionamentos HTTP 301, 302, 307, 308, encaminhamento de caminho, query strings, HTTPS e portas personalizadas.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"es\">\n          <p>redirect.center es un servicio gratuito de redirecci&oacute;n de dominios basado en DNS. Redirija URLs, dominios y subdominios usando solo registros CNAME. Sin necesidad de configurar un servidor &mdash; solo apunte su DNS. Soporta redirecciones HTTP 301, 302, 307, 308, reenv&iacute;o de rutas, cadenas de consulta, HTTPS y puertos personalizados.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"de\">\n          <p>redirect.center ist ein kostenloser DNS-basierter Domain-Weiterleitungsdienst. Leiten Sie URLs, Domains und Subdomains nur mit CNAME-Eintr&auml;gen um. Keine Serverkonfiguration erforderlich &mdash; einfach DNS einstellen und fertig. Unterst&uuml;tzt HTTP 301, 302, 307, 308 Weiterleitungen, Pfadweiterleitung, Query-Strings, HTTPS und benutzerdefinierte Ports.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"fr\">\n          <p>redirect.center est un service gratuit de redirection de domaines bas&eacute; sur le DNS. Redirigez des URL, domaines et sous-domaines en utilisant uniquement des enregistrements CNAME. Aucune configuration de serveur requise &mdash; pointez simplement votre DNS. Prend en charge les redirections HTTP 301, 302, 307, 308, le transfert de chemin, les cha&icirc;nes de requ&ecirc;te, HTTPS et les ports personnalis&eacute;s.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"it\">\n          <p>redirect.center &#232; un servizio gratuito di reindirizzamento domini basato su DNS. Reindirizza URL, domini e sottodomini usando solo record CNAME. Nessuna configurazione del server richiesta &mdash; basta puntare il DNS. Supporta reindirizzamenti HTTP 301, 302, 307, 308, inoltro del percorso, stringhe di query, HTTPS e porte personalizzate.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"ja\">\n          <p>redirect.center&#12399;&#12289;DNS&#12505;&#12540;&#12473;&#12398;&#28961;&#26009;&#12489;&#12513;&#12452;&#12531;&#12522;&#12480;&#12452;&#12524;&#12463;&#12488;&#12469;&#12540;&#12499;&#12473;&#12391;&#12377;&#12290;CNAME&#12524;&#12467;&#12540;&#12489;&#12398;&#12415;&#12434;&#20351;&#29992;&#12375;&#12390;&#12289;URL&#12289;&#12489;&#12513;&#12452;&#12531;&#12289;&#12469;&#12502;&#12489;&#12513;&#12452;&#12531;&#12434;&#12522;&#12480;&#12452;&#12524;&#12463;&#12488;&#12391;&#12365;&#12414;&#12377;&#12290;</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"ru\">\n          <p>redirect.center &mdash; &#1073;&#1077;&#1089;&#1087;&#1083;&#1072;&#1090;&#1085;&#1099;&#1081; &#1089;&#1077;&#1088;&#1074;&#1080;&#1089; &#1087;&#1077;&#1088;&#1077;&#1085;&#1072;&#1087;&#1088;&#1072;&#1074;&#1083;&#1077;&#1085;&#1080;&#1103; &#1076;&#1086;&#1084;&#1077;&#1085;&#1086;&#1074; &#1085;&#1072; &#1086;&#1089;&#1085;&#1086;&#1074;&#1077; DNS. &#1055;&#1077;&#1088;&#1077;&#1085;&#1072;&#1087;&#1088;&#1072;&#1074;&#1083;&#1103;&#1081;&#1090;&#1077; URL-&#1072;&#1076;&#1088;&#1077;&#1089;&#1072;, &#1076;&#1086;&#1084;&#1077;&#1085;&#1099; &#1080; &#1087;&#1086;&#1076;&#1076;&#1086;&#1084;&#1077;&#1085;&#1099;, &#1080;&#1089;&#1087;&#1086;&#1083;&#1100;&#1079;&#1091;&#1103; &#1090;&#1086;&#1083;&#1100;&#1082;&#1086; &#1079;&#1072;&#1087;&#1080;&#1089;&#1080; CNAME.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"ko\">\n          <p>redirect.center&#45716; DNS &#44592;&#48152;&#51032; &#47924;&#47308; &#46020;&#47700;&#51064; &#47532;&#45796;&#51060;&#47113;&#49496; &#49436;&#48708;&#49828;&#51077;&#45768;&#45796;. CNAME &#47112;&#53076;&#46300;&#47564;&#51004;&#47196; URL, &#46020;&#47700;&#51064;, &#49436;&#48652;&#46020;&#47700;&#51064;&#51012; &#47532;&#45796;&#51060;&#47113;&#49496;&#54624; &#49688; &#51080;&#49845;&#45768;&#45796;.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"zh\">\n          <p>redirect.center &#26159;&#19968;&#20010;&#22522;&#20110; DNS &#30340;&#20813;&#36153;&#22495;&#21517;&#37325;&#23450;&#21521;&#26381;&#21153;&#12290;&#20165;&#20351;&#29992; CNAME &#35760;&#24405;&#21363;&#21487;&#37325;&#23450;&#21521; URL&#12289;&#22495;&#21517;&#21644;&#23376;&#22495;&#21517;&#12290;</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"ar\">\n          <p>redirect.center &#1607;&#1608; &#1582;&#1583;&#1605;&#1577; &#1605;&#1580;&#1575;&#1606;&#1610;&#1577; &#1604;&#1573;&#1593;&#1575;&#1583;&#1577; &#1578;&#1608;&#1580;&#1610;&#1607; &#1575;&#1604;&#1606;&#1591;&#1575;&#1602;&#1575;&#1578; &#1593;&#1576;&#1585; DNS. &#1602;&#1605; &#1576;&#1573;&#1593;&#1575;&#1583;&#1577; &#1578;&#1608;&#1580;&#1610;&#1607; &#1593;&#1606;&#1575;&#1608;&#1610;&#1606; URL &#1608;&#1575;&#1604;&#1606;&#1591;&#1575;&#1602;&#1575;&#1578; &#1576;&#1575;&#1587;&#1578;&#1582;&#1583;&#1575;&#1605; &#1587;&#1580;&#1604;&#1575;&#1578; CNAME &#1601;&#1602;&#1591;.</p>\n        </div>\n        <div class=\"footer-seo-block\" lang=\"hi\">\n          <p>redirect.center &#2319;&#2325; &#2350;&#2369;&#2347;&#2381;&#2340; DNS-&#2310;&#2343;&#2366;&#2352;&#2367;&#2340; &#2337;&#2379;&#2350;&#2375;&#2344; &#2352;&#2368;&#2337;&#2366;&#2351;&#2352;&#2375;&#2325;&#2381;&#2335; &#2360;&#2375;&#2357;&#2366; &#2361;&#2376;&#2404; &#2325;&#2375;&#2357;&#2354; CNAME &#2352;&#2367;&#2325;&#2377;&#2352;&#2381;&#2337; &#2325;&#2366; &#2313;&#2346;&#2351;&#2379;&#2327; &#2325;&#2352;&#2325;&#2375; URL &#2352;&#2368;&#2337;&#2366;&#2351;&#2352;&#2375;&#2325;&#2381;&#2335; &#2325;&#2352;&#2375;&#2306;&#2404;</p>\n        </div>\n      </div>\n\n      <div class=\"footer-bottom\">\n        &copy; {{ app.projectName }} &middot; Open source under MIT License &middot;\n        <a href=\"https://github.com/udleinati/redirect.center\">GitHub</a>\n      </div>\n    </div>\n  </footer>\n\n  <!-- CNAME Generator Modal -->\n  <div class=\"modal-overlay\" id=\"modalOverlay\" onclick=\"if(event.target===this)closeModal()\">\n    <div class=\"modal\">\n      <button class=\"modal-close\" onclick=\"closeModal()\">&times;</button>\n      <h2>&#9889; CNAME Generator</h2>\n      <p class=\"modal-desc\">\n        <span class=\"en\">Paste your destination URL and get the CNAME value ready to use in your DNS.</span>\n        <span class=\"pt\">Cole a URL de destino e obtenha o valor CNAME pronto para usar no seu DNS.</span>\n      </p>\n      <label for=\"url\">\n        <span class=\"en\">Destination URL</span>\n        <span class=\"pt\">URL de destino</span>\n      </label>\n      <input type=\"url\" id=\"url\" placeholder=\"https://www.example.com/path?query=value\" autocomplete=\"off\" />\n      <div class=\"converter-arrow\">&#8595;</div>\n      <label for=\"cname\">\n        <span class=\"en\">CNAME value for your DNS</span>\n        <span class=\"pt\">Valor CNAME para seu DNS</span>\n      </label>\n      <div class=\"input-group\">\n        <input type=\"text\" id=\"cname\" readonly />\n        <button class=\"btn-copy\" id=\"copyCNAME\" type=\"button\">Copy</button>\n      </div>\n      <div class=\"modal-tip\">\n        <span class=\"en\"><strong>Redirecting a root/bare domain?</strong> You also need an <strong>A record</strong> pointing to <code>{{ app.entryIp }}</code> and a CNAME on the <code>redirect</code> subdomain.</span>\n        <span class=\"pt\"><strong>Redirecionando um dom&iacute;nio raiz?</strong> Voc&ecirc; tamb&eacute;m precisa de um <strong>registro A</strong> apontando para <code>{{ app.entryIp }}</code> e um CNAME no subdom&iacute;nio <code>redirect</code>.</span>\n      </div>\n    </div>\n  </div>\n\n  <script>\n    // Language\n    function setLang(lang) {\n      document.body.setAttribute('data-lang', lang);\n      document.documentElement.setAttribute('lang', lang);\n      document.getElementById('lang-en').classList.toggle('active', lang === 'en');\n      document.getElementById('lang-pt').classList.toggle('active', lang === 'pt');\n      localStorage.setItem('lang', lang);\n    }\n\n    (function () {\n      var saved = localStorage.getItem('lang');\n      if (saved) { setLang(saved); return; }\n      var browserLang = (navigator.language || '').toLowerCase();\n      setLang(browserLang.startsWith('pt') ? 'pt' : 'en');\n    })();\n\n    // Modal\n    function openModal() {\n      document.getElementById('modalOverlay').classList.add('open');\n      setTimeout(function () { document.getElementById('url').focus(); }, 100);\n    }\n\n    function closeModal() {\n      document.getElementById('modalOverlay').classList.remove('open');\n    }\n\n    document.addEventListener('keydown', function (e) {\n      if (e.key === 'Escape') closeModal();\n    });\n\n    // Accordion\n    function toggleExample(header) {\n      header.parentElement.classList.toggle('open');\n    }\n\n\n    // CNAME converter\n    document.addEventListener('DOMContentLoaded', function () {\n      var urlInput = document.getElementById('url');\n      var cnameOutput = document.getElementById('cname');\n      var copyBtn = document.getElementById('copyCNAME');\n\n      urlInput.addEventListener('input', function () {\n        cnameOutput.value = transformURLtoCNAME(this.value);\n      });\n\n      copyBtn.addEventListener('click', function () {\n        if (!cnameOutput.value) return;\n        navigator.clipboard.writeText(cnameOutput.value).then(function () {\n          copyBtn.textContent = 'Copied!';\n          copyBtn.classList.add('copied');\n          setTimeout(function () {\n            copyBtn.textContent = 'Copy';\n            copyBtn.classList.remove('copied');\n          }, 2000);\n        });\n      });\n\n      function transformURLtoCNAME(urlStr) {\n        if (!urlStr) return '';\n        var url;\n        try { url = new URL(urlStr); } catch (e) { return ''; }\n\n        var result = url.hostname;\n\n        if (url.pathname && url.pathname !== '/') {\n          if (!url.pathname.match(/^[\\/a-z0-9\\-_\\.]+$/) || url.pathname.match(/query/)) {\n            result += '.opts-path-' + base32.encode(\n              new TextEncoder().encode(url.pathname),\n              { type: 'rfc4648', lc: true }\n            );\n          } else {\n            result += url.pathname\n              .replace(/\\//g, '.opts-slash.')\n              .replace(/([^.])$/, '$1')\n              .replace(/\\.$/, '');\n          }\n        }\n\n        if (url.search) {\n          result += '.opts-query-' + base32.encode(\n            new TextEncoder().encode(url.search.slice(1)),\n            { type: 'rfc4648', lc: true }\n          );\n        }\n\n        if (url.protocol === 'https:') {\n          result += '.opts-https';\n        }\n\n        result += '.{{ app.fqdn }}.';\n        return result;\n      }\n    });\n  </script>\n</body>\n\n</html>\n"
  }
]