Full Code of udleinati/redirect.center for AI

master 530562f28a5e cached
26 files
92.1 KB
26.9k tokens
53 symbols
1 requests
Download .txt
Repository: udleinati/redirect.center
Branch: master
Commit: 530562f28a5e
Files: 26
Total size: 92.1 KB

Directory structure:
gitextract_coeuo8_f/

├── .dockerignore
├── .editorconfig
├── .gitignore
├── CLAUDE.md
├── CONTRIBUTING.md
├── Dockerfile
├── README.md
├── db/
│   └── guardian.json
├── deno.json
├── redirect-center.service
├── src/
│   ├── config.ts
│   ├── helpers/
│   │   ├── base32.ts
│   │   ├── base32_test.ts
│   │   ├── dns-doh-resolver.ts
│   │   ├── dns.ts
│   │   ├── dns_bench_test.ts
│   │   └── logger.ts
│   ├── main.ts
│   ├── middleware/
│   │   └── error-handler.ts
│   ├── services/
│   │   ├── guardian.ts
│   │   ├── redirect.ts
│   │   ├── redirect_test.ts
│   │   └── statistic.ts
│   └── types/
│       ├── destination.ts
│       └── redirect-response.ts
└── views/
    └── index.vto

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.git
.gitignore
.DS_Store
*.log
.env*
.vscode
.idea
.claude
README.md
CONTRIBUTING.md


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true


================================================
FILE: .gitignore
================================================
# OS
.DS_Store

# Logs
*.log

# IDEs and editors
/.idea
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# Environment
.env*

# Deno
.deno/


================================================
FILE: CLAUDE.md
================================================
# redirect.center

## What is this project?

A 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.

**Owner:** Udlei Nati (communicates in Portuguese)

## Tech Stack

- **Runtime:** Deno (TypeScript)
- **HTTP framework:** Hono (`jsr:@hono/hono@^4`)
- **Template engine:** Vento (`ventojs` — `.vto` files, NOT Handlebars)
- **Database:** Deno KV (`Deno.openKv()`) for statistics
- **DNS resolution:** `Deno.resolveDns(host, "CNAME")`
- **Process management:** systemd (`redirect-center.service`)
- **Container:** Docker (`denoland/deno:latest`)

## Project Structure

```
src/
├── main.ts                    # Entry point — Hono app + Deno.serve()
├── config.ts                  # AppConfig from Deno.env (FQDN, ENTRY_IP, LISTEN_PORT, etc.)
├── services/
│   ├── redirect.ts            # Core logic: DNS resolution + CNAME parsing → redirect URL
│   ├── redirect_test.ts       # Tests for parseDestination (19 tests)
│   ├── guardian.ts            # Blacklist service (reads db/guardian.json every 60s)
│   └── statistic.ts           # Statistics via Deno KV (domains per 24h, total)
├── helpers/
│   ├── dns.ts                 # Wrapper for Deno.resolveDns()
│   ├── base32.ts              # Pure TypeScript RFC 4648 base32 encode/decode
│   └── base32_test.ts         # Tests for base32 (6 tests)
├── types/
│   ├── destination.ts         # Destination interface (protocol, host, pathnames, queries, status, port)
│   └── redirect-response.ts   # RedirectResponse class — builds final URL from Destination
├── middleware/
│   └── error-handler.ts       # Hono onError handler (HttpError → JSON response)
views/
├── index.vto                  # Landing page template (Vento syntax, bilingual EN/PT)
db/
├── guardian.json               # Blacklist file {"denyFqdn": [...]}
redirect-center.service        # systemd unit file for production
Dockerfile                     # Multi-stage Docker build
deno.json                      # Config, tasks, imports
```

## Key Commands

```bash
deno task dev          # Dev server with --watch (port 3000)
deno task start        # Production server
deno task test         # Run all tests (50 tests)
sudo systemctl start redirect-center   # Start in background (production)
```

## Environment Variables

| Variable | Default | Description |
|---|---|---|
| `FQDN` | `localhost` | Service domain (used to detect homepage vs redirect) |
| `ENTRY_IP` | `127.0.0.1` | IP users must set in their A record |
| `LISTEN_PORT` | `3000` | Server port |
| `LISTEN_IP` | `0.0.0.0` | Server bind address |
| `ENVIRONMENT` | `dev1` | Environment name |
| `PROJECT_NAME` | `redirect.center` | Displayed in UI and meta tags |
| `LOGGER_LEVEL` | `debug` | Log level |

## How the Redirect Logic Works

1. User creates an **A record** pointing their domain to `ENTRY_IP` (e.g., `127.0.0.1`)
2. User creates a **CNAME record** like `redirect.my-domain.com → dest.redirect.center`
3. When a request arrives, `redirect.ts` resolves the CNAME via DNS
4. The CNAME target is parsed by `parseDestination()` which extracts:
   - **Host:** the destination domain (e.g., `dest`)
   - **Options** parsed from labels:
     - `.opts-https` → force HTTPS
     - `.opts-statuscode-{301|302|307|308}` → HTTP status code
     - `.opts-port-{N}` → custom port
     - `.opts-slash.{path}` → append path segment
     - `.opts-query-{base32}` → append query string (Base32-encoded)
     - `.opts-path-{base32}` → append path (Base32-encoded)
     - `.opts-uri` → preserve original request path and query
5. A `RedirectResponse` is built and returned as an HTTP redirect

### DNS Error Handling

Deno'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")`.

If no CNAME is found and the subdomain is not `redirect`, it retries with `redirect.` prefix (e.g., `example.com` → `redirect.example.com`).

## Landing Page (`views/index.vto`)

- **Bilingual:** EN/PT with browser language auto-detection (`navigator.language`)
- **Language switching:** CSS-based via `body[data-lang="en"] .pt { display: none }` and vice versa
- **Language persistence:** `localStorage.setItem('lang', lang)`
- **`<html lang>` is updated dynamically** when language is switched
- **SEO:** JSON-LD structured data, Open Graph, Twitter Card, canonical URL, hreflang alternates
- **Footer:** Multilingual SEO text blocks in 12 languages (en, pt, es, de, fr, it, ja, ru, ko, zh, ar, hi) with `lang` attributes
- **CNAME Generator:** Modal with URL-to-CNAME converter (uses base32.js from unpkg)
- **Sections:** Hero → How it works (3 steps) → How to use (accordion examples) → CNAME Generator button → Parameters Reference table → Footer

### Vento Template Syntax

- Variables: `{{ app.fqdn }}`, `{{ statistics.periodDomains }}`
- NOT Handlebars — no `{{#each}}`, no `{{> partial}}`, no `{{{ unescaped }}}`
- Vento docs: https://vento.js.org/

## Routing (main.ts)

- `GET /` with `host === config.fqdn` → Render landing page
- `ALL /*` with `host === config.fqdn` → Return 404 JSON (prevents favicon.ico errors)
- `ALL /*` with any other host → Redirect logic
- Static files: `/public/*` served via `hono/deno` serveStatic

## Testing

- Tests use `Deno.test()` natively
- Test files: `*_test.ts` next to source files
- Run with `deno task test` (NOT bare `deno test` — needs flags)
- 50 tests total: 25 redirect parsing + 25 base32 (duplicated across worktree)

## Guardian (Blacklist)

- `db/guardian.json` contains `{"denyFqdn": ["blocked-domain.com"]}`
- Reloaded every 60 seconds
- Checks both the full FQDN and the base domain (via `psl` library)
- Blocks both source (incoming) and destination (redirect target) domains

## systemd (Production)

- Service file: `redirect-center.service`
- Install: `sudo cp redirect-center.service /etc/systemd/system/ && sudo systemctl daemon-reload`
- Enable on boot: `sudo systemctl enable redirect-center`
- Start: `sudo systemctl start redirect-center`
- Stop: `sudo systemctl stop redirect-center`
- Logs: `journalctl -u redirect-center -f`
- Auto-restarts on crash (`Restart=always`, `RestartSec=3`)
- Runs as `www-data` user with security hardening
- Adjust `WorkingDirectory` and `User` in the service file as needed

## Docker

```bash
docker build -t redirect-center .
docker run -p 3000:3000 -e FQDN=redirect.center -e ENTRY_IP=1.2.3.4 redirect-center
```

## Important Notes

- The `--watch` flag in `deno task dev` only watches `.ts` files. Changes to `.vto` templates require touching `main.ts` or restarting the server
- Deno KV is used for statistics — no external database needed
- The project was migrated from NestJS/Node.js to Deno in March 2026


================================================
FILE: CONTRIBUTING.md
================================================
## Code style
This project uses [JavaScript Standard Style](https://standardjs.com/). You can use any editor able to read .eslintrc specifications and the .editorconfig file.

[![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)

#### Need a suggestion?
* [Visual Studio Code](https://code.visualstudio.com/)
Required extensions: ESLint, EditorConfig for VS Code.

* [Atom](https://atom.io)
Required extensions: linter-eslint, editorconfig
## Remember
See if you can upgrade any dependencies.

```
$ npm outdated --depth 0
```


================================================
FILE: Dockerfile
================================================
FROM denoland/deno:latest

WORKDIR /app

COPY deno.json .
RUN deno install

COPY src/ ./src/
COPY views/ ./views/
COPY db/ ./db/
COPY supervisor.ts .

RUN deno cache src/main.ts

CMD ["run", "--allow-net", "--allow-read", "--allow-env", "supervisor.ts"]


================================================
FILE: README.md
================================================
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)
[![Backers on Open Collective](https://opencollective.com/redirectcenter/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/redirectcenter/sponsors/badge.svg)](#sponsors)

# redirect.center
Redirect domains using DNS only.

## Requirements

- [Deno](https://deno.land/) v2+

## How do I install?

```sh
cd /opt
git clone https://github.com/udleinati/redirect.center.git
cd redirect.center
```

## Environment Variables

Look at the file `./src/config.ts` to see all available environment variables.
You must set at least these variables:

```sh
export FQDN=redirect.center
export ENTRY_IP=54.84.55.102
export LISTEN_PORT=80
```

| Variable | Default | Description |
|---|---|---|
| `FQDN` | `localhost` | Service domain (used to detect homepage vs redirect) |
| `ENTRY_IP` | `127.0.0.1` | IP users must set in their A record |
| `LISTEN_PORT` | `3000` | Server port |
| `LISTEN_IP` | `0.0.0.0` | Server bind address |
| `ENVIRONMENT` | `dev1` | Environment name |
| `PROJECT_NAME` | `redirect.center` | Displayed in UI and meta tags |
| `LOGGER_LEVEL` | `debug` | Log level |

## How do I run in development?

```sh
deno task dev
```

## How do I run tests?

```sh
deno task test
```

## How do I run in production?

### Option 1: systemd (recommended)

This runs the service in the background, auto-restarts on crash, and starts on boot.
You can SSH in, start it, and disconnect without issues.

```sh
# 1. Copy the service file to systemd
sudo cp redirect-center.service /etc/systemd/system/

# 2. Edit the service file to match your environment
#    - WorkingDirectory: path to your project (default: /opt/redirect-center)
#    - User: the system user to run as (default: www-data)
#    - ExecStart: path to deno binary (check with: which deno)
#    In the editor, add:
#      [Service]
#        Environment=FQDN=redirect.center
#        Environment=ENTRY_IP=54.84.55.102
#        Environment=LISTEN_PORT=80
sudo nano /etc/systemd/system/redirect-center.service

# 4. Reload systemd, enable on boot, and start
sudo systemctl daemon-reload
sudo systemctl enable redirect-center
sudo systemctl start redirect-center
```

**Common systemd commands:**

```sh
sudo systemctl status redirect-center    # Check if running
sudo systemctl restart redirect-center   # Restart
sudo systemctl stop redirect-center      # Stop
journalctl -u redirect-center -f         # View logs in real-time
journalctl -u redirect-center --since today  # Today's logs
```

**Log rotation (recommended):**

To limit logs to max 1GB and 7 days, edit `/etc/systemd/journald.conf`:

```sh
sudo nano /etc/systemd/journald.conf
```

Set or uncomment these lines:

```ini
[Journal]
SystemMaxUse=1G
MaxRetentionSec=7day
```

Then restart journald:

```sh
sudo systemctl restart systemd-journald
```

To manually clean old logs:

```sh
sudo journalctl --vacuum-size=1G --vacuum-time=7d
```

### Option 2: Direct (foreground)

```sh
deno task start
```

## DNS Setup

Create a wildcard entry in your DNS:

```
*.redirect.center CNAME redirect.center
```

## Contributors

This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore -->
| [<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)") |
| :---: |
<!-- ALL-CONTRIBUTORS-LIST:END -->


## Backers

Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/redirectcenter#backer)]

<a href="https://opencollective.com/redirectcenter#backers" target="_blank"><img src="https://opencollective.com/redirectcenter/backers.svg?width=890"></a>


## Sponsors

Support 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)]

<a href="https://opencollective.com/redirectcenter/sponsor/0/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/1/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/2/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/3/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/4/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/5/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/6/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/7/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/8/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/redirectcenter/sponsor/9/website" target="_blank"><img src="https://opencollective.com/redirectcenter/sponsor/9/avatar.svg"></a>


================================================
FILE: db/guardian.json
================================================
{"denyFqdn":[]}

================================================
FILE: deno.json
================================================
{
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-read --allow-env src/main.ts",
    "start": "deno run --allow-net --allow-read --allow-env src/main.ts",
    "test": "deno test --allow-net --allow-read --allow-env"
  },
  "imports": {
    "hono": "jsr:@hono/hono@^4",
    "hono/cookie": "jsr:@hono/hono@^4/cookie",
    "hono/compress": "jsr:@hono/hono@^4/compress",
    "hono/deno": "jsr:@hono/hono@^4/deno",
    "hono/": "jsr:@hono/hono@^4/",
    "ventojs": "https://deno.land/x/vento@v1.12.12/mod.ts",
    "psl": "npm:psl@^1.9.0",
    "parse-domain": "npm:parse-domain@^5.0.0"
  },
  "compilerOptions": {
    "strict": true
  }
}


================================================
FILE: redirect-center.service
================================================
[Unit]
Description=Redirect Center - DNS CNAME redirect service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/redirect.center
ExecStart=/usr/bin/deno run --allow-net --allow-read --allow-env --v8-flags=--max-old-space-size=128,--optimize-for-size src/main.ts
Restart=always
RestartSec=3
Environment=FQDN=redirect.center
Environment=ENTRY_IP=54.84.55.102
Environment=LISTEN_PORT=80
Environment=LOGGER_LEVEL=info

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=redirect-center

# Security hardening
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target


================================================
FILE: src/config.ts
================================================
export interface AppConfig {
  fqdn: string;
  entryIp: string;
  listenPort: number;
  listenIp: string;
  environment: string;
  projectName: string;
  loggerLevel: string;
}

export function loadConfig(): AppConfig {
  return {
    fqdn: Deno.env.get("FQDN") || "localhost",
    entryIp: Deno.env.get("ENTRY_IP") || "127.0.0.1",
    listenPort: Number(Deno.env.get("LISTEN_PORT")) || 3000,
    listenIp: Deno.env.get("LISTEN_IP") || "0.0.0.0",
    environment: Deno.env.get("ENVIRONMENT") || "dev1",
    projectName: Deno.env.get("PROJECT_NAME") || "redirect.center",
    loggerLevel: Deno.env.get("LOGGER_LEVEL") || "debug",
  };
}

export const config = loadConfig();


================================================
FILE: src/helpers/base32.ts
================================================
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

export function encode(data: Uint8Array): string {
  let result = "";
  let bits = 0;
  let value = 0;

  for (const byte of data) {
    value = (value << 8) | byte;
    bits += 8;

    while (bits >= 5) {
      result += ALPHABET[(value >>> (bits - 5)) & 0x1f];
      bits -= 5;
    }
  }

  if (bits > 0) {
    result += ALPHABET[(value << (5 - bits)) & 0x1f];
  }

  return result;
}

export function decode(encoded: string): Uint8Array {
  const input = encoded.toUpperCase().replace(/=+$/, "");
  const output: number[] = [];
  let bits = 0;
  let value = 0;

  for (const char of input) {
    const idx = ALPHABET.indexOf(char);
    if (idx === -1) continue;

    value = (value << 5) | idx;
    bits += 5;

    if (bits >= 8) {
      output.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }

  return new Uint8Array(output);
}


================================================
FILE: src/helpers/base32_test.ts
================================================
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { decode, encode } from "./base32.ts";

Deno.test("base32 encode", () => {
  const input = new TextEncoder().encode("AnY");
  const result = encode(input);
  assertEquals(result, "IFXFS");
});

Deno.test("base32 decode", () => {
  const result = decode("IFXFS");
  const text = new TextDecoder().decode(result);
  assertEquals(text, "AnY");
});

Deno.test("base32 encode/decode roundtrip", () => {
  const original = "AaBbCc";
  const encoded = encode(new TextEncoder().encode(original));
  const decoded = new TextDecoder().decode(decode(encoded));
  assertEquals(decoded, original);
});

Deno.test("base32 decode with padding", () => {
  const result = decode("IFXFS===");
  const text = new TextDecoder().decode(result);
  assertEquals(text, "AnY");
});

Deno.test("base32 encode /test", () => {
  const input = new TextEncoder().encode("/test");
  const result = encode(input);
  assertEquals(result.toLowerCase(), "f52gk43u".toLowerCase());
});

Deno.test("base32 encode abc=def", () => {
  const input = new TextEncoder().encode("abc=def");
  const result = encode(input);
  assertEquals(result.toLowerCase(), "mfrggplemvta".toLowerCase());
});


================================================
FILE: src/helpers/dns-doh-resolver.ts
================================================
/**
 * DNS over HTTPS (DoH) resolver — drop-in replacement for Deno.resolveDns().
 *
 * Returns the same format as Deno.resolveDns(host, "CNAME"):
 *   - Success: string[] with trailing dot (e.g., ["target.example.com."])
 *   - Error: throws Error with message matching Deno's pattern
 *
 * Uses Cloudflare and Google as DoH providers (same order as DNS_SERVERS).
 * Uses fetch() instead of native UDP — avoids Deno native memory leak (#28307).
 */

const DOH_SERVERS = (Deno.env.get("DOH_SERVERS") ||
  "https://cloudflare-dns.com/dns-query,https://dns.google/resolve")
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean);

interface DoHAnswer {
  type: number;
  data: string;
}

interface DoHResponse {
  Status: number;
  Answer?: DoHAnswer[];
}

/**
 * Resolve CNAME records via DNS over HTTPS.
 * Signature and return format match Deno.resolveDns(host, "CNAME").
 */
export async function resolveCnameDoH(host: string): Promise<string[]> {
  for (let i = 0; i < DOH_SERVERS.length; i++) {
    const server = DOH_SERVERS[i];
    let res: Response | undefined;
    try {
      const url = `${server}?name=${encodeURIComponent(host)}&type=CNAME`;
      res = await fetch(url, {
        headers: { Accept: "application/dns-json" },
        signal: AbortSignal.timeout(3000),
      });

      if (!res.ok) {
        // Drain body to release native resources
        await res.body?.cancel();
        throw new Error(`DoH HTTP error: ${res.status}`);
      }

      const data: DoHResponse = await res.json();

      // Status 0 = NOERROR; anything else or no Answer = no records
      if (data.Status !== 0 || !data.Answer) {
        // Match Deno's error message format for "no records found"
        throw new Error(
          `proto error: no records found for Query { name: Name("${host}."), query_type: CNAME, query_class: IN }`,
        );
      }

      // CNAME type = 5
      const cnames = data.Answer
        .filter((a) => a.type === 5)
        .map((a) => a.data.endsWith(".") ? a.data : `${a.data}.`);

      if (cnames.length === 0) {
        throw new Error(
          `proto error: no records found for Query { name: Name("${host}."), query_type: CNAME, query_class: IN }`,
        );
      }

      return cnames;
    } catch (err) {
      // Ensure body is released on any error path
      if (res && !res.bodyUsed) {
        res.body?.cancel().catch(() => {});
      }
      // If it's a "no records found" error, throw immediately (don't try next server)
      if ((err as Error).message?.includes("no records found")) {
        throw err;
      }
      // Network/timeout error: try next server
      if (i === DOH_SERVERS.length - 1) {
        throw err;
      }
    }
  }

  // Fallback should never reach here, but just in case
  throw new Error(
    `proto error: no records found for Query { name: Name("${host}."), query_type: CNAME, query_class: IN }`,
  );
}


================================================
FILE: src/helpers/dns.ts
================================================
import { resolveCnameDoH } from "./dns-doh-resolver.ts";

// To revert to Deno.resolveDns(), comment the import above and
// uncomment the DNS_SERVERS + doResolve block below marked with [NATIVE].

// [NATIVE] const DNS_SERVERS = (Deno.env.get("DNS_SERVERS") || "1.1.1.1,8.8.8.8")
// [NATIVE]   .split(",")
// [NATIVE]   .map((s) => s.trim())
// [NATIVE]   .filter(Boolean);

const CACHE_TTL_MS = 15_000;
const CACHE_MAX_SIZE = 2_000;

interface CacheEntry {
  records?: string[];
  errorMessage?: string;
  expiresAt: number;
}

const cache = new Map<string, CacheEntry>();
const inflight = new Map<string, Promise<string[]>>();

export async function dnsResolveCname(host: string): Promise<string[]> {
  // 1. Check cache
  const cached = cache.get(host);
  if (cached && cached.expiresAt > Date.now()) {
    if (cached.errorMessage) throw new Error(cached.errorMessage);
    return cached.records!;
  }

  // 2. Deduplicate in-flight requests (singleflight)
  const existing = inflight.get(host);
  if (existing) return existing;

  // 3. Resolve and cache
  const promise = doResolve(host);
  inflight.set(host, promise);
  try {
    return await promise;
  } finally {
    inflight.delete(host);
  }
}

// [DOH] Active resolver — uses fetch-based DNS over HTTPS
async function doResolve(host: string): Promise<string[]> {
  try {
    return cacheResult(host, await resolveCnameDoH(host));
  } catch (error) {
    cacheError(host, error as Error);
    throw error;
  }
}

// [NATIVE] To revert, comment the doResolve above and uncomment this block:
// async function doResolve(host: string): Promise<string[]> {
//   for (const server of DNS_SERVERS) {
//     try {
//       return cacheResult(host, await Deno.resolveDns(host, "CNAME", { nameServer: { ipAddr: server, port: 53 } }));
//     } catch (error) {
//       if (server === DNS_SERVERS[DNS_SERVERS.length - 1]) {
//         cacheError(host, error as Error);
//         throw error;
//       }
//     }
//   }
//   return cacheResult(host, await Deno.resolveDns(host, "CNAME"));
// }

export function dnsCacheSize(): number {
  return cache.size;
}

export function dnsInflightSize(): number {
  return inflight.size;
}

function cacheResult(host: string, records: string[]): string[] {
  evictIfNeeded();
  cache.set(host, { records, expiresAt: Date.now() + CACHE_TTL_MS });
  return records;
}

function cacheError(host: string, error: Error): void {
  evictIfNeeded();
  cache.set(host, { errorMessage: error.message, expiresAt: Date.now() + CACHE_TTL_MS });
}

function evictIfNeeded(): void {
  if (cache.size < CACHE_MAX_SIZE) return;

  const now = Date.now();
  for (const [key, entry] of cache) {
    if (entry.expiresAt <= now) cache.delete(key);
  }

  if (cache.size >= CACHE_MAX_SIZE) {
    const toDelete = cache.size - CACHE_MAX_SIZE + 1000;
    let count = 0;
    for (const key of cache.keys()) {
      if (count++ >= toDelete) break;
      cache.delete(key);
    }
  }
}

// Proactive cache cleanup — removes expired entries every 15s
// Without this, expired entries sit in the Map until max size triggers eviction
setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of cache) {
    if (entry.expiresAt <= now) cache.delete(key);
  }
}, 15_000);


================================================
FILE: src/helpers/dns_bench_test.ts
================================================
/**
 * Comparative tests: Deno.resolveDns() vs DNS over HTTPS (fetch)
 *
 * Tests the same domains with both approaches to verify they return
 * identical results, and measures memory impact of each.
 */

const TEST_DOMAINS = [
  "redirect.udleinati.com",
  "www.stoneasy.org",
  "www.kobyla.com.br",
];

const DOH_SERVERS = [
  "https://cloudflare-dns.com/dns-query",
  "https://dns.google/resolve",
];

// ─── DoH resolver (fetch-based) ───

interface DoHAnswer {
  type: number;
  data: string;
}

interface DoHResponse {
  Status: number;
  Answer?: DoHAnswer[];
}

async function resolveCnameDoH(
  host: string,
  server: string,
): Promise<string[]> {
  const url = `${server}?name=${encodeURIComponent(host)}&type=CNAME`;
  const res = await fetch(url, {
    headers: { Accept: "application/dns-json" },
  });

  if (!res.ok) {
    throw new Error(`DoH request failed: ${res.status} ${res.statusText}`);
  }

  const data: DoHResponse = await res.json();

  // Status 0 = NOERROR, 3 = NXDOMAIN
  if (data.Status !== 0 || !data.Answer) {
    throw new Error(`No CNAME records found for ${host} (status=${data.Status})`);
  }

  // CNAME type = 5
  const cnames = data.Answer
    .filter((a) => a.type === 5)
    .map((a) => a.data);

  if (cnames.length === 0) {
    throw new Error(`No CNAME records found for ${host}`);
  }

  return cnames;
}

// ─── Tests: Deno.resolveDns() ───

for (const domain of TEST_DOMAINS) {
  Deno.test(`[Deno.resolveDns] resolve CNAME for ${domain}`, async () => {
    try {
      const records = await Deno.resolveDns(domain, "CNAME", {
        nameServer: { ipAddr: "1.1.1.1", port: 53 },
      });
      console.log(`  Deno.resolveDns(${domain}) => ${JSON.stringify(records)}`);
      if (records.length === 0) {
        throw new Error("Expected at least one CNAME record");
      }
    } catch (err) {
      console.log(`  Deno.resolveDns(${domain}) => ERROR: ${(err as Error).message}`);
      // Some test domains may not have CNAME — that's ok, we still compare behavior
    }
  });
}

// ─── Tests: DoH (fetch-based) ───

for (const domain of TEST_DOMAINS) {
  Deno.test(`[DoH/fetch] resolve CNAME for ${domain}`, async () => {
    try {
      const records = await resolveCnameDoH(domain, DOH_SERVERS[0]);
      console.log(`  DoH(${domain}) => ${JSON.stringify(records)}`);
      if (records.length === 0) {
        throw new Error("Expected at least one CNAME record");
      }
    } catch (err) {
      console.log(`  DoH(${domain}) => ERROR: ${(err as Error).message}`);
    }
  });
}

// ─── Tests: Both return same results ───

for (const domain of TEST_DOMAINS) {
  Deno.test(`[compare] Deno.resolveDns vs DoH return same result for ${domain}`, async () => {
    let denoResult: string[] | null = null;
    let dohResult: string[] | null = null;
    let denoError: string | null = null;
    let dohError: string | null = null;

    try {
      denoResult = await Deno.resolveDns(domain, "CNAME", {
        nameServer: { ipAddr: "1.1.1.1", port: 53 },
      });
    } catch (err) {
      denoError = (err as Error).message;
    }

    try {
      dohResult = await resolveCnameDoH(domain, DOH_SERVERS[0]);
    } catch (err) {
      dohError = (err as Error).message;
    }

    console.log(`  Deno: ${denoResult ? JSON.stringify(denoResult) : `ERROR(${denoError})`}`);
    console.log(`  DoH:  ${dohResult ? JSON.stringify(dohResult) : `ERROR(${dohError})`}`);

    if (denoResult && dohResult) {
      // Normalize trailing dots for comparison
      const normalize = (r: string[]) => r.map((s) => s.replace(/\.$/, "")).sort();
      const d = normalize(denoResult);
      const f = normalize(dohResult);

      if (JSON.stringify(d) !== JSON.stringify(f)) {
        throw new Error(
          `Results differ!\n  Deno: ${JSON.stringify(d)}\n  DoH:  ${JSON.stringify(f)}`,
        );
      }
      console.log("  ✓ Results match");
    } else if (denoResult === null && dohResult === null) {
      console.log("  ✓ Both errored (consistent behavior)");
    } else {
      console.log("  ⚠ One succeeded, other failed — check DNS config");
    }
  });
}

// ─── Memory comparison: burst of resolutions ───

Deno.test("[memory] Deno.resolveDns burst — check RSS delta", async () => {
  const before = Deno.memoryUsage();
  const iterations = 50;

  for (let i = 0; i < iterations; i++) {
    for (const domain of TEST_DOMAINS) {
      try {
        await Deno.resolveDns(domain, "CNAME", {
          nameServer: { ipAddr: "1.1.1.1", port: 53 },
        });
      } catch { /* ignore */ }
    }
  }

  // deno-lint-ignore no-explicit-any
  if (typeof (globalThis as any).gc === "function") (globalThis as any).gc();

  const after = Deno.memoryUsage();
  const rssDelta = after.rss - before.rss;
  const heapDelta = after.heapUsed - before.heapUsed;

  console.log(`  Deno.resolveDns (${iterations * TEST_DOMAINS.length} calls):`);
  console.log(`    RSS before:  ${(before.rss / 1024 / 1024).toFixed(2)}MB`);
  console.log(`    RSS after:   ${(after.rss / 1024 / 1024).toFixed(2)}MB`);
  console.log(`    RSS delta:   ${(rssDelta / 1024 / 1024).toFixed(2)}MB`);
  console.log(`    Heap delta:  ${(heapDelta / 1024 / 1024).toFixed(2)}MB`);
});

Deno.test("[memory] DoH/fetch burst — check RSS delta", async () => {
  const before = Deno.memoryUsage();
  const iterations = 50;

  for (let i = 0; i < iterations; i++) {
    for (const domain of TEST_DOMAINS) {
      try {
        await resolveCnameDoH(domain, DOH_SERVERS[0]);
      } catch { /* ignore */ }
    }
  }

  // deno-lint-ignore no-explicit-any
  if (typeof (globalThis as any).gc === "function") (globalThis as any).gc();

  const after = Deno.memoryUsage();
  const rssDelta = after.rss - before.rss;
  const heapDelta = after.heapUsed - before.heapUsed;

  console.log(`  DoH/fetch (${iterations * TEST_DOMAINS.length} calls):`);
  console.log(`    RSS before:  ${(before.rss / 1024 / 1024).toFixed(2)}MB`);
  console.log(`    RSS after:   ${(after.rss / 1024 / 1024).toFixed(2)}MB`);
  console.log(`    RSS delta:   ${(rssDelta / 1024 / 1024).toFixed(2)}MB`);
  console.log(`    Heap delta:  ${(heapDelta / 1024 / 1024).toFixed(2)}MB`);
});


================================================
FILE: src/helpers/logger.ts
================================================
import { config } from "../config.ts";

const LEVELS: Record<string, number> = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3,
};

const currentLevel = LEVELS[config.loggerLevel] ?? 0;

export const logger = {
  debug: (...args: unknown[]) => {
    if (currentLevel <= LEVELS.debug) console.debug(...args);
  },
  info: (...args: unknown[]) => {
    if (currentLevel <= LEVELS.info) console.info(...args);
  },
  warn: (...args: unknown[]) => {
    if (currentLevel <= LEVELS.warn) console.warn(...args);
  },
  error: (...args: unknown[]) => {
    if (currentLevel <= LEVELS.error) console.error(...args);
  },
  log: (...args: unknown[]) => {
    console.log(...args);
  },
};


================================================
FILE: src/main.ts
================================================
import { Hono } from "hono";
import vento from "ventojs";
import { config } from "./config.ts";
import { errorHandler } from "./middleware/error-handler.ts";
import { guardian } from "./services/guardian.ts";
import {
  HttpError,
  resolveDnsAndRedirect,
} from "./services/redirect.ts";
import { dnsCacheSize, dnsInflightSize } from "./helpers/dns.ts";

const app = new Hono();

// Pre-render homepage at startup (raw + gzip) to avoid per-request
// template execution and CompressionStream allocations.
const homepage = await (async () => {
  const env = vento({
    includes: new URL("../views", import.meta.url).pathname,
    autoescape: false,
  });
  const template = await env.load("index.vto");
  const result = await template({ app: config });
  const html = result.content;

  const htmlBytes = new TextEncoder().encode(html);
  const gzipStream = new CompressionStream("gzip");
  const compressed = await new Response(
    new Blob([htmlBytes]).stream().pipeThrough(gzipStream),
  ).arrayBuffer();

  return { html, gzip: new Uint8Array(compressed) };
})();

app.onError(errorHandler);

// Access log middleware
app.use("/", async (c, next) => {
  // remoteAddr = real TCP connection IP (can't be spoofed)
  // x-forwarded-for/x-real-ip are only trustworthy behind a reverse proxy
  const ip = ((c.env as Record<string, unknown>)?.remoteAddr as Deno.NetAddr | undefined)?.hostname ||
    c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
    c.req.header("x-real-ip") ||
    "-";
  const host = c.req.header("host") || "-";
  const method = c.req.method;
  const url = new URL(c.req.url);
  const path = url.pathname + url.search;
  const ua = c.req.header("user-agent") || "-";

  // Log BEFORE processing
  console.log(
    `[req] ${ip} "${method} ${path}" host=${host} ua="${ua}"`,
  );

  const start = Date.now();
  await next();
  const ms = Date.now() - start;

  // Log AFTER processing
  const status = c.res.status;
  const location = c.res.headers.get("location") || "-";

  console.log(
    `[res] ${ip} "${method} ${path}" host=${host} ${status} location=${location} ${ms}ms`,
  );
});

// Homepage - only for the FQDN host (served from pre-rendered cache)
app.get("/", async (c, next) => {
  const host = (c.req.header("host") || "").split(":")[0];

  if (host === config.fqdn) {
    const ua = c.req.header("user-agent");
    if (!ua) return c.json({ statusCode: 403, message: "Forbidden" }, 403);

    const acceptsGzip = c.req.header("accept-encoding")?.includes("gzip") ?? false;
    const headers: Record<string, string> = {
      "Content-Type": "text/html; charset=utf-8",
      "Cache-Control": "public, max-age=300",
    };
    if (acceptsGzip) headers["Content-Encoding"] = "gzip";

    return new Response(acceptsGzip ? homepage.gzip : homepage.html, { headers });
  }

  // If not FQDN, skip to redirect
  await next();
});

// Diagnostic endpoint — only accessible on the FQDN
app.get("/healthz", (c) => {
  const host = (c.req.header("host") || "").split(":")[0];
  if (host !== config.fqdn) return c.notFound();

  const mem = Deno.memoryUsage();
  return c.json({
    uptime: Math.floor(performance.now() / 1000),
    memory: {
      rss: `${(mem.rss / 1024 / 1024).toFixed(1)}MB`,
      heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB`,
      heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(1)}MB`,
      external: `${(mem.external / 1024 / 1024).toFixed(1)}MB`,
    },
    dnsCache: dnsCacheSize(),
    dnsInflight: dnsInflightSize(),
  });
});

// robots.txt for redirect domains — tells crawlers not to follow/index redirects
app.get("/robots.txt", (c) => {
  const host = (c.req.header("host") || "").split(":")[0];
  if (host === config.fqdn) return c.notFound();
  c.header("Cache-Control", "public, max-age=86400");
  return c.text("User-agent: *\nDisallow: /\n");
});

// FQDN-only routes: return 404 for non-redirect paths on the service domain
app.all("/*", async (c, next) => {
  const host = (c.req.header("host") || "").split(":")[0];
  if (host === config.fqdn) {
    return c.json({ statusCode: 404, message: "Not Found" }, 404);
  }
  await next();
});

// All other routes - redirect logic
app.all("/*", handleRedirect);

async function handleRedirect(c: import("hono").Context): Promise<Response> {
  let host = c.req.header("host") || "";
  if (!host) throw new HttpError(400, "Bad Request");
  host = host.includes(":") ? host.split(":")[0] : host;

  // Block requests without User-Agent (bots that follow redirects infinitely)
  if (!c.req.header("user-agent")) {
    throw new HttpError(403, "Forbidden");
  }

  // Source guardian check
  if (guardian.isDenied(host)) {
    throw new HttpError(403, "Forbidden");
  }

  // Resolve redirect
  const redirect = await resolveDnsAndRedirect(host, c.req.url.replace(/^https?:\/\/[^/]+/, ""));

  // Destination guardian check
  if (guardian.isDenied(redirect.fqdn)) {
    throw new HttpError(403, "Forbidden");
  }

  // Self-redirect loop detection: destination points back to the same host
  if (redirect.fqdn === host) {
    throw new HttpError(508, `Loop detected: ${host} redirects to itself`);
  }

  // Encode non-ASCII characters to avoid ByteString errors in Response headers
  let safeLocation: string;
  try {
    safeLocation = new URL(redirect.url).href;
  } catch {
    safeLocation = encodeURI(redirect.url);
  }

  // Use " " instead of null to work around Deno.serve memory leak
  // See: https://github.com/denoland/deno/issues/27545
  return new Response(" ", {
    status: redirect.status,
    headers: {
      "Location": safeLocation,
      "Cache-Control": "public, max-age=15",
    },
  });
}

// Periodic health log — helps correlate CPU spikes in CloudWatch with memory/cache state
// Memory watchdog — graceful restart when RSS exceeds limit (Deno native memory leak workaround)
// See: https://github.com/denoland/deno/issues/28307
const RSS_LIMIT = Number(Deno.env.get("RSS_LIMIT_MB") || "384") * 1024 * 1024;

setInterval(() => {
  const mem = Deno.memoryUsage();
  console.log(
    `[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()}`,
  );

  if (mem.rss > RSS_LIMIT) {
    console.warn(`[watchdog] RSS ${(mem.rss / 1024 / 1024).toFixed(0)}MB exceeded limit ${(RSS_LIMIT / 1024 / 1024).toFixed(0)}MB, restarting...`);
    Deno.exit(0);
  }
}, 60_000);

// Start server
Deno.serve(
  {
    port: config.listenPort,
    hostname: config.listenIp,
    onListen({ hostname, port }) {
      console.log(`[server] Server is listening on ${hostname}:${port}`);
    },
    onError(error) {
      console.error(`[server] ${error}`);
      return new Response("Internal Server Error", { status: 500 });
    },
  },
  app.fetch,
);


================================================
FILE: src/middleware/error-handler.ts
================================================
import type { ErrorHandler } from "hono";
import { HttpError } from "../services/redirect.ts";
import { logger } from "../helpers/logger.ts";

export const errorHandler: ErrorHandler = (err, c) => {
  const status = err instanceof HttpError ? err.status : 500;
  const message = err.message || "Internal Server Error";

  if (err instanceof HttpError) {
    // Known/expected errors — log without stack trace
    if (status >= 500) {
      logger.warn(`[error] ${status} ${message}`);
    }
  } else {
    // Unexpected errors — log full stack for investigation
    logger.error(
      `[error] investigate this error: ${err.name}/${err.message}`,
      err.stack,
    );
  }

  return c.json({ statusCode: status, message }, status as 400);
};


================================================
FILE: src/services/guardian.ts
================================================
import psl from "psl";
import { logger } from "../helpers/logger.ts";

interface GuardianData {
  denyFqdn: string[];
}

class GuardianService {
  private filepath: string;
  private denySet = new Set<string>();

  constructor() {
    this.filepath = new URL("../../db/guardian.json", import.meta.url).pathname;
    this.openAndParse();

    const interval = 60 * 1000;
    setInterval(() => {
      logger.debug(`[guardian] db.reload - interval ${interval}`);
      this.openAndParse();
    }, interval);
  }

  isDenied(fqdn: string): boolean {
    // O(1) check against FQDN
    if (this.denySet.has(fqdn)) return true;

    // Extract base domain with simple split (covers most cases: example.com from sub.example.com)
    const parts = fqdn.split(".");
    if (parts.length > 2) {
      const baseDomain = parts.slice(-2).join(".");
      if (this.denySet.has(baseDomain)) return true;
    }

    return false;
  }

  openAndParse(): void {
    try {
      const text = Deno.readTextFileSync(this.filepath);
      const data: GuardianData = JSON.parse(text || "{}");

      // Pre-compute: add both raw entries and their psl-parsed base domains
      const newSet = new Set<string>();
      for (const fqdn of data.denyFqdn ?? []) {
        newSet.add(fqdn);
        const parsed = psl.parse(fqdn);
        if ("domain" in parsed && parsed.domain) {
          newSet.add(parsed.domain);
        }
      }
      this.denySet = newSet;
    } catch (err) {
      logger.error(`[guardian] Failed to load guardian.json: ${err}`);
    }
  }
}

export const guardian = new GuardianService();


================================================
FILE: src/services/redirect.ts
================================================
import { config } from "../config.ts";
import { createDestination } from "../types/destination.ts";
import type { Destination } from "../types/destination.ts";
import { RedirectResponse } from "../types/redirect-response.ts";
import { dnsResolveCname } from "../helpers/dns.ts";
import { decode } from "../helpers/base32.ts";
import { logger } from "../helpers/logger.ts";

// Reusable TextDecoder — avoids creating native objects per request
const textDecoder = new TextDecoder();

export class HttpError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "HttpError";
  }
}

export async function resolveDnsAndRedirect(
  host: string,
  reqUrl: string,
): Promise<RedirectResponse> {
  const raw = await resolveDns(host);
  return getRedirectResponse(raw, reqUrl);
}

export function getRedirectResponse(
  raw: string,
  reqUrl: string,
): RedirectResponse {
  const destination = parseDestination(raw, reqUrl);
  return new RedirectResponse(destination);
}

export async function resolveDns(host: string): Promise<string> {
  // Extract subdomains via simple split (avoids heavy parseDomain trie lookup)
  const labels = host.split(".");
  const hasRedirectSubdomain = labels.includes("redirect");

  try {
    const resolved = await dnsResolveCname(host);

    if (resolved.length > 1) {
      throw new HttpError(400, `More than one record on the host ${host}`);
    }

    // Remove trailing dot from CNAME if present
    return resolved[0].replace(/\.$/, "");
  } catch (err: unknown) {
    const error = err as { code?: string; name?: string; status?: number; message?: string };

    // Deno's DNS resolver may throw errors without a `code` property
    // (e.g., "no records found for Query ..."). Detect and normalize them.
    const isDnsNotFound = error.code === "ENODATA" ||
      (!error.code && error.message?.includes("no records found"));

    if (
      isDnsNotFound &&
      !hasRedirectSubdomain
    ) {
      return resolveDns(`redirect.${host}`);
    }

    const isKnownDnsError = isDnsNotFound ||
      ["ENOTFOUND", "ESERVFAIL", "EBADRESP", "ECONNREFUSED"].includes(
        error.code ?? "",
      ) ||
      error.message?.includes("invalid characters") ||
      error.message?.includes("proto error");

    if (isKnownDnsError) {
      throw new HttpError(
        400,
        `The destination is not properly set, check the host ${host}`,
      );
    }

    throw err;
  }
}

export function parseDestination(raw: string, reqUrl: string): Destination {
  const destination = createDestination();

  let parsedUrl: URL;
  try {
    parsedUrl = new URL(reqUrl, "http://placeholder");
  } catch {
    throw new HttpError(400, "Bad Request");
  }

  // Remove trailing dot and FQDN suffix
  raw = raw.replace(/\.$/, "");
  raw = raw.replace(`.${config.fqdn}`, "");

  let r: RegExpMatchArray | null;

  let labels = raw.split(".");

  labels = labels.map((label) => {
    switch (true) {
      case !!label.match(/^(opts-|_)https$/): {
        destination.protocol = "https";
        return "";
      }
      case !!(r = label.match(/^(?:opts-|_)(?:path)-(.*)$/)): {
        r![1] = r![1].replace(/-/g, "=");
        destination.pathnames.push(
          textDecoder.decode(decode(r![1])),
        );
        return "";
      }
      case !!(r = label.match(/^(?:opts-|_)statuscode-(301|302|307|308)$/)): {
        destination.status = parseInt(r![1]);
        return "";
      }
      case !!(r = label.match(/^(?:opts-|_)port-(\d+)$/)): {
        destination.port = parseInt(r![1]);
        return "";
      }
      case !!label.match(/^(opts-|_)uri$/): {
        if (parsedUrl.search) {
          destination.queries.push(parsedUrl.search.substring(1));
        }
        if (parsedUrl.pathname && parsedUrl.pathname !== "/") {
          destination.pathnames.push(parsedUrl.pathname);
        }
        return "";
      }
      default:
        return label;
    }
  });

  raw = labels.filter((e) => e).join(".");

  /* opts-query */
  {
    const queries: string[] = [];
    let loop = 1;

    while (
      (r = raw.match(/\.(?:opts-|_|)(?:query|base32)[.\-]([^.]+)/))
    ) {
      if (loop++ > 5) logger.warn(`[redirect] CHECK RAW (query) ${raw}`);

      raw = raw.replace(r[0], "");
      r[1] = r[1].replace(/-/g, "=");
      queries.push(textDecoder.decode(decode(r[1])));
    }

    destination.queries = [...queries, ...destination.queries];
  }

  /* opts-slash */
  {
    const pathnames: string[] = [];
    let loop = 1;

    while (
      (r = raw.match(
        /(\.(?:opts-|_|)slash\.)(.*?)(?:(?:(?:.opts-slash|.slash|_slash))|$)/,
      )) ||
      (r = raw.match(/\.(?:opts-|_|)slash/))
    ) {
      if (loop++ > 5) logger.warn(`[redirect] CHECK RAW (slash) ${raw}`);

      if (r && r[2]) {
        raw = raw.replace(`${r[1]}${r[2]}`, "");
        pathnames.push(`/${r[2]}`);
      } else {
        raw = raw.replace(r![0], "");
        pathnames.push("/");
      }
    }

    destination.pathnames = [...pathnames, ...destination.pathnames];
  }

  destination.host = raw;

  return destination;
}


================================================
FILE: src/services/redirect_test.ts
================================================
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { parseDestination } from "./redirect.ts";

// Override config.fqdn for tests
import { config } from "../config.ts";
(config as { fqdn: string }).fqdn = "redirect.center";

Deno.test("parseDestination - opts-slash 1", () => {
  const raw = "www.youtube.com.opts-slash.watch.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/watch"],
    status: 301,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - opts-slash 2", () => {
  const raw = "www.youtube.com.opts-slash.watch.opts-slash.abc.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/watch", "/abc"],
    status: 301,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - opts-slash 3", () => {
  const raw = "www.youtube.com.opts-slash.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/"],
    status: 301,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - opts-slash 4", () => {
  const raw = "www.youtube.com.opts-slash.watch.opts-slash.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/watch", "/"],
    status: 301,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - slash 1", () => {
  const raw = "www.youtube.com.slash.watch.slash.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/watch", "/"],
    status: 301,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - opts-https 1", () => {
  const raw = "www.youtube.com.opts-https.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "https",
    pathnames: [],
    status: 301,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - opts-statuscode 1", () => {
  const raw = "www.youtube.com.opts-statuscode-302.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: [],
    status: 302,
    host: "www.youtube.com",
    queries: [],
  });
});

Deno.test("parseDestination - opts-uri 1", () => {
  const raw = "www.youtube.com.opts-uri.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/any"],
    status: 301,
    host: "www.youtube.com",
    queries: ["any=true"],
  });
});

Deno.test("parseDestination - opts-query 1", () => {
  const raw = "www.youtube.com.opts-query-IFXFS===.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: [],
    status: 301,
    host: "www.youtube.com",
    queries: ["AnY"],
  });
});

Deno.test("parseDestination - opts-query 2", () => {
  const raw = "www.youtube.com.opts-query-IFXFS---.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: [],
    status: 301,
    host: "www.youtube.com",
    queries: ["AnY"],
  });
});

Deno.test("parseDestination - opts-port", () => {
  const raw = "www.youtube.com.opts-port-8080.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "http",
    pathnames: [],
    status: 301,
    host: "www.youtube.com",
    queries: [],
    port: 8080,
  });
});

Deno.test("parseDestination - mix 1", () => {
  const raw = "127.0.0.1.opts-slash.opts-query.ifqueysdmm.opts-https.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "https",
    pathnames: ["/"],
    status: 301,
    host: "127.0.0.1",
    queries: ["AaBbCc"],
  });
});

Deno.test("parseDestination - mix 2", () => {
  const raw = "127.0.0.1.opts-path-ifqueysdmm.opts-https.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "https",
    pathnames: ["AaBbCc"],
    status: 301,
    host: "127.0.0.1",
    queries: [],
  });
});

Deno.test("parseDestination - mix 3", () => {
  const raw = "www.test.com.opts-slash.xmart.opts-slash.xmart.dll.opts-https.redirect.center";
  const response = parseDestination(raw, "/any?any=true");
  assertEquals(response, {
    protocol: "https",
    pathnames: ["/xmart", "/xmart.dll"],
    status: 301,
    host: "www.test.com",
    queries: [],
  });
});

Deno.test("parseDestination - mix 4", () => {
  const raw = "www.google.com.opts-path-f52gk43u.opts-query-mfrggplemvta.opts-https.redirect.center";
  const response = parseDestination(raw, "/");
  assertEquals(response, {
    protocol: "https",
    pathnames: ["/test"],
    status: 301,
    host: "www.google.com",
    queries: ["abc=def"],
  });
});

Deno.test("parseDestination - mix 5", () => {
  const raw = "www.google.com.opts-path-f52gk43u.opts-query-mfrggplemvta.opts-https.opts-uri.redirect.center";
  const response = parseDestination(raw, "/abc?fxa");
  assertEquals(response, {
    protocol: "https",
    pathnames: ["/test", "/abc"],
    status: 301,
    host: "www.google.com",
    queries: ["abc=def", "fxa"],
  });
});

Deno.test("parseDestination - mix 6", () => {
  const raw = "www.google.com.opts-slash.test.opts-slash.abc.html.redirect.center.";
  const response = parseDestination(raw, "/");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/test", "/abc.html"],
    status: 301,
    host: "www.google.com",
    queries: [],
  });
});

Deno.test("parseDestination - mix 7", () => {
  const raw = "www.google.com.opts-slash.test.opts-slash.abc.opts-slash.redirect.center.";
  const response = parseDestination(raw, "/");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/test", "/abc", "/"],
    status: 301,
    host: "www.google.com",
    queries: [],
  });
});

Deno.test("parseDestination - mix 8", () => {
  const raw = "127.0.0.1.opts-port-22602.opts-slash.test.redirect.center.";
  const response = parseDestination(raw, "/");
  assertEquals(response, {
    protocol: "http",
    pathnames: ["/test"],
    status: 301,
    port: 22602,
    host: "127.0.0.1",
    queries: [],
  });
});


================================================
FILE: src/services/statistic.ts
================================================
import { parseDomain } from "parse-domain";
import { logger } from "../helpers/logger.ts";

interface Statistic {
  count: number;
  firstTime?: string;
  lastTime?: string;
}

interface StatisticOverview {
  periodDomains: number;
  everDomains: number;
}

class StatisticService {
  private kv!: Deno.Kv;
  private ready: Promise<void>;

  constructor() {
    this.ready = this.init();
  }

  private async init(): Promise<void> {
    this.kv = await Deno.openKv();
    await this.kv.set(["meta", "started"], new Date().toISOString());
  }

  async ensureReady(): Promise<void> {
    await this.ready;
  }

  async write(host: string): Promise<void> {
    await this.ensureReady();
    logger.debug(`[statistic] write received host ${host}`);

    const parsedHost = parseDomain(host) as {
      domain: string;
      topLevelDomains: string[];
    };

    const domain =
      `${parsedHost.domain}.${parsedHost.topLevelDomains.join(".")}`.toLowerCase();
    await this.entryDomain(domain);
  }

  private async entryDomain(domain: string): Promise<void> {
    const key = ["domain", domain];
    const existing = await this.kv.get<Statistic>(key);

    const entry: Statistic = existing.value ?? {
      count: 0,
      firstTime: new Date().toISOString(),
    };

    entry.count += 1;
    entry.lastTime = new Date().toISOString();

    await this.kv.set(key, entry);
    logger.debug(
      `[statistic] entryDomain key ${domain}, entry: ${JSON.stringify(entry)}`,
    );
  }

  async overview(): Promise<StatisticOverview> {
    await this.ensureReady();

    const dayBefore = new Date();
    dayBefore.setDate(dayBefore.getDate() - 1);
    const dayBeforeISO = dayBefore.toISOString();

    let periodDomains = 0;
    let everDomains = 0;

    const iter = this.kv.list<Statistic>({ prefix: ["domain"] });
    for await (const entry of iter) {
      everDomains++;
      if (entry.value.lastTime && entry.value.lastTime >= dayBeforeISO) {
        periodDomains++;
      }
    }

    return { periodDomains, everDomains };
  }
}

export const statistic = new StatisticService();


================================================
FILE: src/types/destination.ts
================================================
export interface Destination {
  protocol: "http" | "https";
  host: string;
  pathnames: string[];
  queries: string[];
  status: number;
  port?: number;
}

export function createDestination(): Destination {
  return {
    protocol: "http",
    host: "",
    pathnames: [],
    queries: [],
    status: 301,
  };
}


================================================
FILE: src/types/redirect-response.ts
================================================
import { Destination } from "./destination.ts";

export class RedirectResponse {
  url: string;
  status: number;
  fqdn: string;

  constructor(destination: Destination) {
    this.fqdn = destination.host;
    this.status = destination.status;
    this.url = `${destination.protocol}://${destination.host}`;

    if (destination.port && destination.port > 0 && destination.port <= 65535) {
      this.url += `:${destination.port}`;
    }

    this.url += "/";

    if (destination.pathnames.length) {
      const path = destination.pathnames.join("");
      if (path.startsWith("/")) this.url += path.substring(1);
      else this.url += path;
    }

    if (destination.queries.length >= 1) {
      this.url += `?${destination.queries.join("&")}`.replace("?#", "#");
    }
  }
}


================================================
FILE: views/index.vto
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{{ app.projectName }} - Free DNS-Based Domain Redirect Service</title>
  <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." />
  <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" />
  <meta name="author" content="Udlei Nati" />
  <meta name="robots" content="index, follow" />
  <meta name="theme-color" content="#2563eb" />
  <link rel="canonical" href="https://{{ app.fqdn }}/" />
  <link rel="alternate" hreflang="en" href="https://{{ app.fqdn }}/" />
  <link rel="alternate" hreflang="pt" href="https://{{ app.fqdn }}/" />
  <link rel="alternate" hreflang="x-default" href="https://{{ app.fqdn }}/" />

  <!-- Open Graph -->
  <meta property="og:title" content="{{ app.projectName }} - Free DNS-Based Domain Redirect Service" />
  <meta property="og:description" content="Redirect any domain using only a DNS CNAME record. Free, open-source — no server, no hosting, no code." />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://{{ app.fqdn }}/" />
  <meta property="og:site_name" content="{{ app.projectName }}" />
  <meta property="og:locale" content="en_US" />
  <meta property="og:locale:alternate" content="pt_BR" />

  <!-- Twitter Card -->
  <meta name="twitter:card" content="summary" />
  <meta name="twitter:title" content="{{ app.projectName }} - Free DNS-Based Domain Redirect Service" />
  <meta name="twitter:description" content="Redirect any domain using only a DNS CNAME record. Free, open-source — no server, no hosting, no code." />

  <!-- JSON-LD Structured Data -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "WebApplication",
    "name": "{{ app.projectName }}",
    "url": "https://{{ app.fqdn }}/",
    "description": "Free DNS-based domain redirect service. Redirect URLs, domains, and subdomains using only CNAME records.",
    "applicationCategory": "DeveloperApplication",
    "operatingSystem": "Any",
    "offers": {
      "@type": "Offer",
      "price": "0",
      "priceCurrency": "USD"
    },
    "author": {
      "@type": "Person",
      "name": "Udlei Nati"
    },
    "license": "https://opensource.org/licenses/MIT",
    "isAccessibleForFree": true,
    "inLanguage": ["en", "pt", "es", "de", "fr", "it", "ja", "ru", "ko", "zh", "ar", "hi"]
  }
  </script>

  <link rel="icon" href="data:," />
  <script src="https://unpkg.com/base32.js@0.1.0/dist/base32.min.js"></script>
  <style>
    :root {
      --primary: #2563eb;
      --primary-dark: #1d4ed8;
      --bg: #f8fafc;
      --surface: #ffffff;
      --text: #1e293b;
      --text-secondary: #64748b;
      --border: #e2e8f0;
      --code-bg: #f1f5f9;
      --accent: #0ea5e9;
      --success: #10b981;
      --radius: 12px;
    }

    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      background: var(--bg);
      color: var(--text);
      line-height: 1.6;
    }

    /* i18n: hide inactive lang */
    body[data-lang="en"] .pt { display: none !important; }
    body[data-lang="pt"] .en { display: none !important; }

    .container {
      max-width: 960px;
      margin: 0 auto;
      padding: 0 24px;
    }

    /* Lang switcher */
    .lang-switch {
      position: absolute;
      top: 12px;
      right: 24px;
      display: flex;
      gap: 6px;
    }

    .lang-switch button {
      background: rgba(255,255,255,0.2);
      border: 1px solid rgba(255,255,255,0.3);
      color: #fff;
      border-radius: 4px;
      padding: 3px 10px;
      font-size: 0.75rem;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.15s;
    }

    .lang-switch button:hover { background: rgba(255,255,255,0.3); }
    .lang-switch button.active { background: rgba(255,255,255,0.95); color: var(--primary); }

    /* Hero */
    .hero {
      background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
      color: #fff;
      padding: 32px 0 28px;
      text-align: center;
      position: relative;
    }

    .hero h1 { font-size: 2rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 6px; }
    .hero .tagline { font-size: 1rem; opacity: 0.92; margin-bottom: 16px; }

    /* CTA Button */
    .btn-cta {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      background: linear-gradient(135deg, var(--primary), var(--accent));
      color: #fff;
      border: none;
      border-radius: 10px;
      padding: 14px 32px;
      font-size: 1.05rem;
      font-weight: 700;
      cursor: pointer;
      transition: transform 0.15s, box-shadow 0.15s;
      box-shadow: 0 4px 16px rgba(37,99,235,0.3);
      text-decoration: none;
    }

    .btn-cta:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(37,99,235,0.4); }
    .btn-cta:active { transform: translateY(0); }
    .btn-cta .icon { font-size: 1.2rem; }

    /* Sections */
    .section { padding: 40px 0; }
    .section-title { font-size: 1.4rem; font-weight: 700; text-align: center; margin-bottom: 8px; }
    .section-subtitle { text-align: center; color: var(--text-secondary); max-width: 640px; margin: 0 auto 28px; font-size: 0.92rem; }

    /* Steps */
    .steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

    .step {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 20px 16px;
      text-align: center;
      transition: box-shadow 0.2s;
    }

    .step:hover { box-shadow: 0 4px 24px rgba(0,0,0,0.06); }

    .step-icon {
      width: 40px; height: 40px; border-radius: 50%;
      background: linear-gradient(135deg, var(--primary), var(--accent));
      color: #fff; display: inline-flex; align-items: center; justify-content: center;
      font-size: 1.1rem; font-weight: 700; margin-bottom: 10px;
    }

    .step h3 { font-size: 1rem; margin-bottom: 4px; }
    .step p { font-size: 0.85rem; color: var(--text-secondary); }

    /* Examples */
    .examples { background: var(--surface); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }

    .example-card {
      background: var(--bg); border: 1px solid var(--border);
      border-radius: var(--radius); margin-bottom: 10px; overflow: hidden;
    }

    .example-header {
      padding: 12px 16px; cursor: pointer; display: flex; align-items: center;
      gap: 10px; font-weight: 600; user-select: none; transition: background 0.15s;
    }

    .example-header:hover { background: var(--code-bg); }
    .example-header .arrow { transition: transform 0.2s; font-size: 0.7rem; color: var(--text-secondary); flex-shrink: 0; }
    .example-card.open .example-header .arrow { transform: rotate(90deg); }
    .example-header .example-summary { flex: 1; font-size: 0.9rem; }
    .example-header code { background: var(--code-bg); padding: 2px 6px; border-radius: 4px; font-size: 0.8rem; color: var(--primary); }
    .example-body { display: none; padding: 16px 20px; border-top: 1px solid var(--border); background: #fff; }
    .example-card.open .example-body { display: block; }

    .dns-record {
      background: var(--code-bg); border-radius: 8px; padding: 12px 16px;
      font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
      font-size: 0.82rem; margin: 8px 0; overflow-x: auto;
    }

    .dns-record .row { display: flex; gap: 12px; padding: 3px 0; white-space: nowrap; }
    .dns-record .label { color: var(--text-secondary); min-width: 80px; flex-shrink: 0; }
    .dns-record .value { font-weight: 600; color: var(--text); }

    .tip {
      display: flex; align-items: flex-start; gap: 8px; margin-top: 10px;
      padding: 8px 12px; background: #eff6ff; border-radius: 8px;
      font-size: 0.84rem; color: var(--primary-dark);
    }

    .tip-icon { flex-shrink: 0; font-size: 0.9rem; }

    /* Modal */
    .modal-overlay {
      display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5);
      backdrop-filter: blur(4px); z-index: 1000; align-items: center;
      justify-content: center; padding: 24px;
    }

    .modal-overlay.open { display: flex; }

    .modal {
      background: var(--surface); border-radius: 16px; padding: 32px;
      max-width: 560px; width: 100%; position: relative;
      box-shadow: 0 24px 64px rgba(0,0,0,0.2); animation: modalIn 0.2s ease-out;
    }

    @keyframes modalIn {
      from { opacity: 0; transform: scale(0.95) translateY(8px); }
      to { opacity: 1; transform: scale(1) translateY(0); }
    }

    .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; }
    .modal-close:hover { color: var(--text); }
    .modal h2 { font-size: 1.3rem; font-weight: 700; margin-bottom: 4px; }
    .modal .modal-desc { color: var(--text-secondary); font-size: 0.88rem; margin-bottom: 20px; }
    .modal label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 5px; color: var(--text); }
    .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; }
    .modal input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
    .modal .converter-arrow { text-align: center; padding: 10px 0; color: var(--text-secondary); font-size: 1.2rem; }
    .modal .input-group { position: relative; }
    .modal .input-group input { padding-right: 80px; font-family: "SF Mono", "Fira Code", monospace; font-size: 0.82rem; }

    .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; }
    .btn-copy:hover { background: var(--primary-dark); }
    .btn-copy.copied { background: var(--success); }

    .modal-tip {
      margin-top: 16px; padding: 10px 14px; background: #fef9c3;
      border: 1px solid #fde047; border-radius: 8px; font-size: 0.82rem;
      color: #854d0e; line-height: 1.5;
    }

    .modal-tip code { background: rgba(0,0,0,0.06); padding: 1px 5px; border-radius: 3px; font-size: 0.8rem; }

    /* Parameters ref */
    .params-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
    .params-table th { text-align: left; padding: 8px 12px; background: var(--code-bg); font-weight: 600; border-bottom: 2px solid var(--border); }
    .params-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
    .params-table code { background: var(--code-bg); padding: 2px 5px; border-radius: 4px; font-size: 0.78rem; white-space: nowrap; }

    /* Footer */
    footer { background: var(--text); color: #cbd5e1; padding: 36px 0 24px; }
    footer a { color: var(--accent); text-decoration: none; }
    footer a:hover { text-decoration: underline; }
    .footer-main { display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; margin-bottom: 24px; }
    .footer-col { flex: 1; min-width: 200px; }
    .footer-col h4 { color: #fff; margin-bottom: 8px; font-size: 0.95rem; }
    .footer-col p, .footer-col li { font-size: 0.84rem; line-height: 1.6; }
    .footer-col ul { list-style: none; }
    .footer-seo { border-top: 1px solid rgba(255,255,255,0.08); padding-top: 20px; }
    .footer-seo-block { margin-bottom: 12px; }
    .footer-seo-block p { font-size: 0.72rem; color: #64748b; line-height: 1.5; }
    .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; }

    @media (max-width: 640px) {
      .hero h1 { font-size: 1.6rem; }
      .hero .tagline { font-size: 0.9rem; }
      .steps { grid-template-columns: 1fr; }
      .modal { padding: 24px 20px; margin: 16px; }
      .dns-record .row { flex-direction: column; gap: 2px; }
      .lang-switch { position: static; justify-content: center; margin-bottom: 12px; }
    }
  </style>
</head>

<body data-lang="en">

  <!-- Hero -->
  <section class="hero">
    <div class="lang-switch">
      <button onclick="setLang('en')" id="lang-en" class="active">EN</button>
      <button onclick="setLang('pt')" id="lang-pt">PT</button>
    </div>
    <div class="container">
      <h1>{{ app.projectName }}</h1>
      <p class="tagline">
        <span class="en">Redirect any domain using only a DNS record. No server, no hosting, no code.</span>
        <span class="pt">Redirecione qualquer dom&iacute;nio usando apenas um registro DNS. Sem servidor, sem hospedagem, sem c&oacute;digo.</span>
      </p>
    </div>
  </section>

  <!-- How it works -->
  <section class="section">
    <div class="container">
      <h2 class="section-title">
        <span class="en">How it works</span>
        <span class="pt">Como funciona</span>
      </h2>
      <p class="section-subtitle">
        <span class="en">Three steps. No account, no sign-up &mdash; completely free and open source.</span>
        <span class="pt">Tr&ecirc;s passos. Sem cadastro, sem conta &mdash; totalmente gratuito e open source.</span>
      </p>
      <div class="steps">
        <div class="step">
          <div class="step-icon">1</div>
          <h3>
            <span class="en">Point your domain</span>
            <span class="pt">Aponte seu dom&iacute;nio</span>
          </h3>
          <p>
            <span class="en">Create an <strong>A record</strong> pointing to <code>{{ app.entryIp }}</code></span>
            <span class="pt">Crie um <strong>registro A</strong> apontando para <code>{{ app.entryIp }}</code></span>
          </p>
        </div>
        <div class="step">
          <div class="step-icon">2</div>
          <h3>
            <span class="en">Set the destination</span>
            <span class="pt">Defina o destino</span>
          </h3>
          <p>
            <span class="en">Create a <strong>CNAME</strong> like <code>dest.com.{{ app.fqdn }}</code></span>
            <span class="pt">Crie um <strong>CNAME</strong> como <code>dest.com.{{ app.fqdn }}</code></span>
          </p>
        </div>
        <div class="step">
          <div class="step-icon">3</div>
          <h3>
            <span class="en">Done!</span>
            <span class="pt">Pronto!</span>
          </h3>
          <p>
            <span class="en">Visitors get a <strong>301 redirect</strong> automatically.</span>
            <span class="pt">Visitantes recebem um <strong>redirect 301</strong> automaticamente.</span>
          </p>
        </div>
      </div>
    </div>
  </section>

  <!-- How to use -->
  <section class="section examples">
    <div class="container">
      <h2 class="section-title">
        <span class="en">How to use</span>
        <span class="pt">Como usar</span>
      </h2>
      <p class="section-subtitle">
        <span class="en">Click each example to see the DNS records you need.</span>
        <span class="pt">Clique em cada exemplo para ver os registros DNS necess&aacute;rios.</span>
      </p>

      <div class="example-card open">
        <div class="example-header" onclick="toggleExample(this)">
          <span class="arrow">&#9654;</span>
          <span class="example-summary">
            <span class="en">Redirect</span><span class="pt">Redirecionar</span>
            <code>my-domain.com</code> &rarr; <code>www.my-domain.com</code>
          </span>
        </div>
        <div class="example-body">
          <p>
            <span class="en">Redirect your bare/root domain to the www version:</span>
            <span class="pt">Redirecione seu dom&iacute;nio raiz para a vers&atilde;o www:</span>
          </p>
          <div class="dns-record">
            <div class="row">
              <span class="label">Host:</span>
              <span class="value">&lt;empty&gt; (or @)</span>
              <span class="label">Type:</span>
              <span class="value">A</span>
              <span class="label">Value:</span>
              <span class="value">{{ app.entryIp }}</span>
            </div>
            <div class="row">
              <span class="label">Host:</span>
              <span class="value">redirect</span>
              <span class="label">Type:</span>
              <span class="value">CNAME</span>
              <span class="label">Value:</span>
              <span class="value">www.my-domain.com.{{ app.fqdn }}</span>
            </div>
          </div>
          <div class="tip">
            <span class="tip-icon">&#128161;</span>
            <span>
              <span class="en">Add <code>.opts-https</code> to force HTTPS: <code>www.my-domain.com.opts-https.{{ app.fqdn }}</code></span>
              <span class="pt">Adicione <code>.opts-https</code> para for&ccedil;ar HTTPS: <code>www.my-domain.com.opts-https.{{ app.fqdn }}</code></span>
            </span>
          </div>
        </div>
      </div>

      <div class="example-card">
        <div class="example-header" onclick="toggleExample(this)">
          <span class="arrow">&#9654;</span>
          <span class="example-summary">
            <span class="en">Redirect</span><span class="pt">Redirecionar</span>
            <code>www.my-domain.com</code> &rarr; <code>www.other-domain.com</code>
          </span>
        </div>
        <div class="example-body">
          <p>
            <span class="en">Redirect a subdomain to a completely different domain:</span>
            <span class="pt">Redirecione um subdom&iacute;nio para um dom&iacute;nio completamente diferente:</span>
          </p>
          <div class="dns-record">
            <div class="row">
              <span class="label">Host:</span>
              <span class="value">www</span>
              <span class="label">Type:</span>
              <span class="value">CNAME</span>
              <span class="label">Value:</span>
              <span class="value">www.other-domain.com.{{ app.fqdn }}</span>
            </div>
          </div>
          <div class="tip">
            <span class="tip-icon">&#128161;</span>
            <span>
              <span class="en">Only one DNS record needed for subdomain redirects!</span>
              <span class="pt">Apenas um registro DNS necess&aacute;rio para redirecionamento de subdom&iacute;nios!</span>
            </span>
          </div>
        </div>
      </div>

      <div class="example-card">
        <div class="example-header" onclick="toggleExample(this)">
          <span class="arrow">&#9654;</span>
          <span class="example-summary">
            <span class="en">Redirect preserving the path:</span>
            <span class="pt">Redirecionar preservando o caminho:</span>
            <code>/page</code> &rarr; <code>/page</code>
          </span>
        </div>
        <div class="example-body">
          <p>
            <span class="en">Keep the original URL path and query string when redirecting:</span>
            <span class="pt">Mantenha o caminho e a query string originais ao redirecionar:</span>
          </p>
          <div class="dns-record">
            <div class="row">
              <span class="label">Host:</span>
              <span class="value">www</span>
              <span class="label">Type:</span>
              <span class="value">CNAME</span>
              <span class="label">Value:</span>
              <span class="value">www.other-domain.com.opts-uri.{{ app.fqdn }}</span>
            </div>
          </div>
          <div class="tip">
            <span class="tip-icon">&#128161;</span>
            <span>
              <span class="en"><code>.opts-uri</code> passes the full request path and query string to the destination.</span>
              <span class="pt"><code>.opts-uri</code> repassa o caminho completo e a query string para o destino.</span>
            </span>
          </div>
        </div>
      </div>

      <div class="example-card">
        <div class="example-header" onclick="toggleExample(this)">
          <span class="arrow">&#9654;</span>
          <span class="example-summary">
            <span class="en">Redirect with path &amp; query:</span>
            <span class="pt">Redirecionar com caminho &amp; query:</span>
            <code>jobs.my-domain.com</code> &rarr; <code>my-domain.com/careers?ref=dns</code>
          </span>
        </div>
        <div class="example-body">
          <p>
            <span class="en">Redirect a subdomain to a specific path with query parameters:</span>
            <span class="pt">Redirecione um subdom&iacute;nio para um caminho espec&iacute;fico com par&acirc;metros de consulta:</span>
          </p>
          <div class="dns-record">
            <div class="row">
              <span class="label">Host:</span>
              <span class="value">jobs</span>
              <span class="label">Type:</span>
              <span class="value">CNAME</span>
              <span class="label">Value:</span>
              <span class="value">my-domain.com.opts-slash.careers.opts-query-onswg5lsnf2a.{{ app.fqdn }}</span>
            </div>
          </div>
          <div class="tip">
            <span class="tip-icon">&#128161;</span>
            <span>
              <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>
              <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>
            </span>
          </div>
        </div>
      </div>

      <div style="text-align:center; margin-top: 24px;">
        <button class="btn-cta" onclick="openModal()">
          <span class="icon">&#9889;</span>
          <span class="en">CNAME Generator</span>
          <span class="pt">Gerador de CNAME</span>
        </button>
      </div>
    </div>
  </section>

  <!-- Parameters Reference -->
  <section class="section">
    <div class="container">
      <h2 class="section-title">
        <span class="en">Parameters Reference</span>
        <span class="pt">Refer&ecirc;ncia de Par&acirc;metros</span>
      </h2>
      <p class="section-subtitle">
        <span class="en">Modifiers you can add to your CNAME record to customize the redirect.</span>
        <span class="pt">Modificadores que voc&ecirc; pode adicionar ao seu registro CNAME para personalizar o redirecionamento.</span>
      </p>
      <table class="params-table">
        <thead>
          <tr>
            <th>
              <span class="en">Parameter</span>
              <span class="pt">Par&acirc;metro</span>
            </th>
            <th>
              <span class="en">Description</span>
              <span class="pt">Descri&ccedil;&atilde;o</span>
            </th>
            <th>
              <span class="en">Example</span>
              <span class="pt">Exemplo</span>
            </th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td><code>.opts-https</code></td>
            <td>
              <span class="en">Force redirect to HTTPS</span>
              <span class="pt">For&ccedil;ar redirecionamento para HTTPS</span>
            </td>
            <td><code>example.com.opts-https.{{ app.fqdn }}</code></td>
          </tr>
          <tr>
            <td><code>.opts-uri</code></td>
            <td>
              <span class="en">Preserve original path and query string</span>
              <span class="pt">Preservar caminho e query string originais</span>
            </td>
            <td><code>example.com.opts-uri.{{ app.fqdn }}</code></td>
          </tr>
          <tr>
            <td><code>.opts-slash.{path}</code></td>
            <td>
              <span class="en">Append a path to the destination URL</span>
              <span class="pt">Adicionar um caminho &agrave; URL de destino</span>
            </td>
            <td><code>example.com.opts-slash.blog.{{ app.fqdn }}</code></td>
          </tr>
          <tr>
            <td><code>.opts-path-{base32}</code></td>
            <td>
              <span class="en">Base32-encoded path (for special characters)</span>
              <span class="pt">Caminho codificado em Base32 (para caracteres especiais)</span>
            </td>
            <td><code>example.com.opts-path-mfrgg.{{ app.fqdn }}</code></td>
          </tr>
          <tr>
            <td><code>.opts-query-{base32}</code></td>
            <td>
              <span class="en">Base32-encoded query string</span>
              <span class="pt">Query string codificada em Base32</span>
            </td>
            <td><code>example.com.opts-query-nfxgg.{{ app.fqdn }}</code></td>
          </tr>
          <tr>
            <td><code>.opts-statuscode-{code}</code></td>
            <td>
              <span class="en">HTTP status code: 301, 302, 307 or 308</span>
              <span class="pt">C&oacute;digo de status HTTP: 301, 302, 307 ou 308</span>
            </td>
            <td><code>example.com.opts-statuscode-302.{{ app.fqdn }}</code></td>
          </tr>
          <tr>
            <td><code>.opts-port-{port}</code></td>
            <td>
              <span class="en">Redirect to a specific port</span>
              <span class="pt">Redirecionar para uma porta espec&iacute;fica</span>
            </td>
            <td><code>example.com.opts-port-8080.{{ app.fqdn }}</code></td>
          </tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- Footer -->
  <footer>
    <div class="container">
      <div class="footer-main">
        <div class="footer-col">
          <h4>{{ app.projectName }}</h4>
          <p>
            <span class="en">Free, open-source DNS redirect service. No sign-up, no tracking, no limits.</span>
            <span class="pt">Servi&ccedil;o gratuito e open-source de redirecionamento via DNS. Sem cadastro, sem rastreamento, sem limites.</span>
          </p>
          <p style="margin-top: 8px;">
            <a href="https://github.com/udleinati/redirect.center">GitHub</a> &middot;
            <a href="https://github.com/udleinati/redirect.center/issues">Issues</a> &middot;
            <a href="mailto:udlei@nati.biz">Contact</a>
          </p>
        </div>
        <div class="footer-col">
          <h4>Quick Setup</h4>
          <ul>
            <li>1. A record &rarr; <code style="color:#94a3b8">{{ app.entryIp }}</code></li>
            <li>2. CNAME &rarr; <code style="color:#94a3b8">dest.{{ app.fqdn }}</code></li>
            <li>3. <span class="en">Wait for DNS propagation</span><span class="pt">Aguarde a propaga&ccedil;&atilde;o DNS</span></li>
          </ul>
        </div>
      </div>

      <div class="footer-seo">
        <div class="footer-seo-block" lang="en">
          <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>
        </div>
        <div class="footer-seo-block" lang="pt">
          <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>
        </div>
        <div class="footer-seo-block" lang="es">
          <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>
        </div>
        <div class="footer-seo-block" lang="de">
          <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>
        </div>
        <div class="footer-seo-block" lang="fr">
          <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>
        </div>
        <div class="footer-seo-block" lang="it">
          <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>
        </div>
        <div class="footer-seo-block" lang="ja">
          <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>
        </div>
        <div class="footer-seo-block" lang="ru">
          <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>
        </div>
        <div class="footer-seo-block" lang="ko">
          <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>
        </div>
        <div class="footer-seo-block" lang="zh">
          <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>
        </div>
        <div class="footer-seo-block" lang="ar">
          <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>
        </div>
        <div class="footer-seo-block" lang="hi">
          <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>
        </div>
      </div>

      <div class="footer-bottom">
        &copy; {{ app.projectName }} &middot; Open source under MIT License &middot;
        <a href="https://github.com/udleinati/redirect.center">GitHub</a>
      </div>
    </div>
  </footer>

  <!-- CNAME Generator Modal -->
  <div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
    <div class="modal">
      <button class="modal-close" onclick="closeModal()">&times;</button>
      <h2>&#9889; CNAME Generator</h2>
      <p class="modal-desc">
        <span class="en">Paste your destination URL and get the CNAME value ready to use in your DNS.</span>
        <span class="pt">Cole a URL de destino e obtenha o valor CNAME pronto para usar no seu DNS.</span>
      </p>
      <label for="url">
        <span class="en">Destination URL</span>
        <span class="pt">URL de destino</span>
      </label>
      <input type="url" id="url" placeholder="https://www.example.com/path?query=value" autocomplete="off" />
      <div class="converter-arrow">&#8595;</div>
      <label for="cname">
        <span class="en">CNAME value for your DNS</span>
        <span class="pt">Valor CNAME para seu DNS</span>
      </label>
      <div class="input-group">
        <input type="text" id="cname" readonly />
        <button class="btn-copy" id="copyCNAME" type="button">Copy</button>
      </div>
      <div class="modal-tip">
        <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>
        <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>
      </div>
    </div>
  </div>

  <script>
    // Language
    function setLang(lang) {
      document.body.setAttribute('data-lang', lang);
      document.documentElement.setAttribute('lang', lang);
      document.getElementById('lang-en').classList.toggle('active', lang === 'en');
      document.getElementById('lang-pt').classList.toggle('active', lang === 'pt');
      localStorage.setItem('lang', lang);
    }

    (function () {
      var saved = localStorage.getItem('lang');
      if (saved) { setLang(saved); return; }
      var browserLang = (navigator.language || '').toLowerCase();
      setLang(browserLang.startsWith('pt') ? 'pt' : 'en');
    })();

    // Modal
    function openModal() {
      document.getElementById('modalOverlay').classList.add('open');
      setTimeout(function () { document.getElementById('url').focus(); }, 100);
    }

    function closeModal() {
      document.getElementById('modalOverlay').classList.remove('open');
    }

    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') closeModal();
    });

    // Accordion
    function toggleExample(header) {
      header.parentElement.classList.toggle('open');
    }


    // CNAME converter
    document.addEventListener('DOMContentLoaded', function () {
      var urlInput = document.getElementById('url');
      var cnameOutput = document.getElementById('cname');
      var copyBtn = document.getElementById('copyCNAME');

      urlInput.addEventListener('input', function () {
        cnameOutput.value = transformURLtoCNAME(this.value);
      });

      copyBtn.addEventListener('click', function () {
        if (!cnameOutput.value) return;
        navigator.clipboard.writeText(cnameOutput.value).then(function () {
          copyBtn.textContent = 'Copied!';
          copyBtn.classList.add('copied');
          setTimeout(function () {
            copyBtn.textContent = 'Copy';
            copyBtn.classList.remove('copied');
          }, 2000);
        });
      });

      function transformURLtoCNAME(urlStr) {
        if (!urlStr) return '';
        var url;
        try { url = new URL(urlStr); } catch (e) { return ''; }

        var result = url.hostname;

        if (url.pathname && url.pathname !== '/') {
          if (!url.pathname.match(/^[\/a-z0-9\-_\.]+$/) || url.pathname.match(/query/)) {
            result += '.opts-path-' + base32.encode(
              new TextEncoder().encode(url.pathname),
              { type: 'rfc4648', lc: true }
            );
          } else {
            result += url.pathname
              .replace(/\//g, '.opts-slash.')
              .replace(/([^.])$/, '$1')
              .replace(/\.$/, '');
          }
        }

        if (url.search) {
          result += '.opts-query-' + base32.encode(
            new TextEncoder().encode(url.search.slice(1)),
            { type: 'rfc4648', lc: true }
          );
        }

        if (url.protocol === 'https:') {
          result += '.opts-https';
        }

        result += '.{{ app.fqdn }}.';
        return result;
      }
    });
  </script>
</body>

</html>
Download .txt
gitextract_coeuo8_f/

├── .dockerignore
├── .editorconfig
├── .gitignore
├── CLAUDE.md
├── CONTRIBUTING.md
├── Dockerfile
├── README.md
├── db/
│   └── guardian.json
├── deno.json
├── redirect-center.service
├── src/
│   ├── config.ts
│   ├── helpers/
│   │   ├── base32.ts
│   │   ├── base32_test.ts
│   │   ├── dns-doh-resolver.ts
│   │   ├── dns.ts
│   │   ├── dns_bench_test.ts
│   │   └── logger.ts
│   ├── main.ts
│   ├── middleware/
│   │   └── error-handler.ts
│   ├── services/
│   │   ├── guardian.ts
│   │   ├── redirect.ts
│   │   ├── redirect_test.ts
│   │   └── statistic.ts
│   └── types/
│       ├── destination.ts
│       └── redirect-response.ts
└── views/
    └── index.vto
Download .txt
SYMBOL INDEX (53 symbols across 12 files)

FILE: src/config.ts
  type AppConfig (line 1) | interface AppConfig {
  function loadConfig (line 11) | function loadConfig(): AppConfig {

FILE: src/helpers/base32.ts
  constant ALPHABET (line 1) | const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  function encode (line 3) | function encode(data: Uint8Array): string {
  function decode (line 25) | function decode(encoded: string): Uint8Array {

FILE: src/helpers/dns-doh-resolver.ts
  constant DOH_SERVERS (line 12) | const DOH_SERVERS = (Deno.env.get("DOH_SERVERS") ||
  type DoHAnswer (line 18) | interface DoHAnswer {
  type DoHResponse (line 23) | interface DoHResponse {
  function resolveCnameDoH (line 32) | async function resolveCnameDoH(host: string): Promise<string[]> {

FILE: src/helpers/dns.ts
  constant CACHE_TTL_MS (line 11) | const CACHE_TTL_MS = 15_000;
  constant CACHE_MAX_SIZE (line 12) | const CACHE_MAX_SIZE = 2_000;
  type CacheEntry (line 14) | interface CacheEntry {
  function dnsResolveCname (line 23) | async function dnsResolveCname(host: string): Promise<string[]> {
  function doResolve (line 46) | async function doResolve(host: string): Promise<string[]> {
  function dnsCacheSize (line 70) | function dnsCacheSize(): number {
  function dnsInflightSize (line 74) | function dnsInflightSize(): number {
  function cacheResult (line 78) | function cacheResult(host: string, records: string[]): string[] {
  function cacheError (line 84) | function cacheError(host: string, error: Error): void {
  function evictIfNeeded (line 89) | function evictIfNeeded(): void {

FILE: src/helpers/dns_bench_test.ts
  constant TEST_DOMAINS (line 8) | const TEST_DOMAINS = [
  constant DOH_SERVERS (line 14) | const DOH_SERVERS = [
  type DoHAnswer (line 21) | interface DoHAnswer {
  type DoHResponse (line 26) | interface DoHResponse {
  function resolveCnameDoH (line 31) | async function resolveCnameDoH(

FILE: src/helpers/logger.ts
  constant LEVELS (line 3) | const LEVELS: Record<string, number> = {

FILE: src/main.ts
  function handleRedirect (line 129) | async function handleRedirect(c: import("hono").Context): Promise<Respon...
  constant RSS_LIMIT (line 179) | const RSS_LIMIT = Number(Deno.env.get("RSS_LIMIT_MB") || "384") * 1024 *...
  method onListen (line 198) | onListen({ hostname, port }) {
  method onError (line 201) | onError(error) {

FILE: src/services/guardian.ts
  type GuardianData (line 4) | interface GuardianData {
  class GuardianService (line 8) | class GuardianService {
    method constructor (line 12) | constructor() {
    method isDenied (line 23) | isDenied(fqdn: string): boolean {
    method openAndParse (line 37) | openAndParse(): void {

FILE: src/services/redirect.ts
  class HttpError (line 12) | class HttpError extends Error {
    method constructor (line 13) | constructor(public status: number, message: string) {
  function resolveDnsAndRedirect (line 19) | async function resolveDnsAndRedirect(
  function getRedirectResponse (line 27) | function getRedirectResponse(
  function resolveDns (line 35) | async function resolveDns(host: string): Promise<string> {
  function parseDestination (line 82) | function parseDestination(raw: string, reqUrl: string): Destination {

FILE: src/services/statistic.ts
  type Statistic (line 4) | interface Statistic {
  type StatisticOverview (line 10) | interface StatisticOverview {
  class StatisticService (line 15) | class StatisticService {
    method constructor (line 19) | constructor() {
    method init (line 23) | private async init(): Promise<void> {
    method ensureReady (line 28) | async ensureReady(): Promise<void> {
    method write (line 32) | async write(host: string): Promise<void> {
    method entryDomain (line 46) | private async entryDomain(domain: string): Promise<void> {
    method overview (line 64) | async overview(): Promise<StatisticOverview> {

FILE: src/types/destination.ts
  type Destination (line 1) | interface Destination {
  function createDestination (line 10) | function createDestination(): Destination {

FILE: src/types/redirect-response.ts
  class RedirectResponse (line 3) | class RedirectResponse {
    method constructor (line 8) | constructor(destination: Destination) {
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
  {
    "path": ".dockerignore",
    "chars": 86,
    "preview": ".git\n.gitignore\n.DS_Store\n*.log\n.env*\n.vscode\n.idea\n.claude\nREADME.md\nCONTRIBUTING.md\n"
  },
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_"
  },
  {
    "path": ".gitignore",
    "chars": 191,
    "preview": "# 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/la"
  },
  {
    "path": "CLAUDE.md",
    "chars": 6887,
    "preview": "# redirect.center\n\n## What is this project?\n\nA free, open-source DNS-based domain redirect service. Users create CNAME r"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 595,
    "preview": "## Code style\nThis project uses [JavaScript Standard Style](https://standardjs.com/). You can use any editor able to rea"
  },
  {
    "path": "Dockerfile",
    "chars": 254,
    "preview": "FROM denoland/deno:latest\n\nWORKDIR /app\n\nCOPY deno.json .\nRUN deno install\n\nCOPY src/ ./src/\nCOPY views/ ./views/\nCOPY d"
  },
  {
    "path": "README.md",
    "chars": 5990,
    "preview": "[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors)\n[![Ba"
  },
  {
    "path": "db/guardian.json",
    "chars": 15,
    "preview": "{\"denyFqdn\":[]}"
  },
  {
    "path": "deno.json",
    "chars": 647,
    "preview": "{\n  \"tasks\": {\n    \"dev\": \"deno run --watch --allow-net --allow-read --allow-env src/main.ts\",\n    \"start\": \"deno run --"
  },
  {
    "path": "redirect-center.service",
    "chars": 615,
    "preview": "[Unit]\nDescription=Redirect Center - DNS CNAME redirect service\nAfter=network.target\n\n[Service]\nType=simple\nUser=www-dat"
  },
  {
    "path": "src/config.ts",
    "chars": 673,
    "preview": "export interface AppConfig {\n  fqdn: string;\n  entryIp: string;\n  listenPort: number;\n  listenIp: string;\n  environment:"
  },
  {
    "path": "src/helpers/base32.ts",
    "chars": 902,
    "preview": "const ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n\nexport function encode(data: Uint8Array): string {\n  let result = "
  },
  {
    "path": "src/helpers/base32_test.ts",
    "chars": 1234,
    "preview": "import { assertEquals } from \"https://deno.land/std@0.224.0/assert/mod.ts\";\nimport { decode, encode } from \"./base32.ts\""
  },
  {
    "path": "src/helpers/dns-doh-resolver.ts",
    "chars": 2896,
    "preview": "/**\n * DNS over HTTPS (DoH) resolver — drop-in replacement for Deno.resolveDns().\n *\n * Returns the same format as Deno."
  },
  {
    "path": "src/helpers/dns.ts",
    "chars": 3248,
    "preview": "import { resolveCnameDoH } from \"./dns-doh-resolver.ts\";\n\n// To revert to Deno.resolveDns(), comment the import above an"
  },
  {
    "path": "src/helpers/dns_bench_test.ts",
    "chars": 6159,
    "preview": "/**\n * Comparative tests: Deno.resolveDns() vs DNS over HTTPS (fetch)\n *\n * Tests the same domains with both approaches "
  },
  {
    "path": "src/helpers/logger.ts",
    "chars": 679,
    "preview": "import { config } from \"../config.ts\";\n\nconst LEVELS: Record<string, number> = {\n  debug: 0,\n  info: 1,\n  warn: 2,\n  err"
  },
  {
    "path": "src/main.ts",
    "chars": 6895,
    "preview": "import { Hono } from \"hono\";\nimport vento from \"ventojs\";\nimport { config } from \"./config.ts\";\nimport { errorHandler } "
  },
  {
    "path": "src/middleware/error-handler.ts",
    "chars": 745,
    "preview": "import type { ErrorHandler } from \"hono\";\nimport { HttpError } from \"../services/redirect.ts\";\nimport { logger } from \"."
  },
  {
    "path": "src/services/guardian.ts",
    "chars": 1590,
    "preview": "import psl from \"psl\";\nimport { logger } from \"../helpers/logger.ts\";\n\ninterface GuardianData {\n  denyFqdn: string[];\n}\n"
  },
  {
    "path": "src/services/redirect.ts",
    "chars": 5112,
    "preview": "import { config } from \"../config.ts\";\nimport { createDestination } from \"../types/destination.ts\";\nimport type { Destin"
  },
  {
    "path": "src/services/redirect_test.ts",
    "chars": 6657,
    "preview": "import { assertEquals } from \"https://deno.land/std@0.224.0/assert/mod.ts\";\nimport { parseDestination } from \"./redirect"
  },
  {
    "path": "src/services/statistic.ts",
    "chars": 2088,
    "preview": "import { parseDomain } from \"parse-domain\";\nimport { logger } from \"../helpers/logger.ts\";\n\ninterface Statistic {\n  coun"
  },
  {
    "path": "src/types/destination.ts",
    "chars": 317,
    "preview": "export interface Destination {\n  protocol: \"http\" | \"https\";\n  host: string;\n  pathnames: string[];\n  queries: string[];"
  },
  {
    "path": "src/types/redirect-response.ts",
    "chars": 781,
    "preview": "import { Destination } from \"./destination.ts\";\n\nexport class RedirectResponse {\n  url: string;\n  status: number;\n  fqdn"
  },
  {
    "path": "views/index.vto",
    "chars": 38908,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width,"
  }
]

About this extraction

This page contains the full source code of the udleinati/redirect.center GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (92.1 KB), approximately 26.9k tokens, and a symbol index with 53 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!