route-enc
v0.1.0
Published
Encrypted route URLs for Next.js (AES-GCM + proxy rewrite + SecureLink)
Maintainers
Readme
route-enc
Encrypted route URLs for Next.js. Real routes like /dashboard are shared as /x/<token> and rewritten server-side to the original path. AES-256-GCM via WebCrypto, works in Node + Edge (proxy.ts).
Install
npm i route-encRequires Next.js >= 15, React >= 18.
Setup (3 steps)
1. Generate a key
openssl rand -hex 32Add to .env.local (never commit this file):
ROUTE_ENCRYPTION_KEY=3e61dbb7753b30ac286478b7558c225a220b42affa9a5491a682278c0f6e2f9b
# Optional. Default is /x
# ROUTE_PREFIX=/secureKey must be exactly 64 hex characters (32 bytes).
2. Add proxy (Next 16)
Create src/proxy.ts in your app:
import { createEncryptedProxy } from "route-enc/proxy";
export const proxy = createEncryptedProxy();
export const config = {
matcher: ["/x/:path*"],
};If you use a custom prefix, change both places:
export const proxy = createEncryptedProxy({ prefix: "/secure" });
export const config = {
matcher: ["/secure/:path*"],
};3. Use SecureLink (Server Component only)
import { SecureLink } from "route-enc/react";
export default function Page() {
return <SecureLink href="/dashboard">Dashboard</SecureLink>;
}This renders <a href="/x/TulP7bbq...">. The proxy decrypts it and rewrites to /dashboard. The real path never appears in HTML.
Do NOT add
"use client"to files usingSecureLink. It encrypts at render time so your key never ships to the browser.
Manual encrypt / decrypt
For redirects, server actions, or API routes:
import { encryptRoute, decryptRoute, buildSecureHref } from "route-enc";
// Server-side only (needs the key)
const token = await encryptRoute("/dashboard?tab=1");
const href = buildSecureHref(token); // "/x/<token>"
// Decrypt
const path = await decryptRoute(token); // "/dashboard?tab=1" or null if tamperedRedirect example:
import { redirect } from "next/navigation";
import { encryptRoute, buildSecureHref } from "route-enc";
export async function GET() {
const token = await encryptRoute("/dashboard");
redirect(buildSecureHref(token));
}Pass key/prefix explicitly (no env):
await encryptRoute("/dashboard", { key: "your-64-hex-key" });
await decryptRoute(token, { key: "your-64-hex-key" });API
| Import | Description |
|---|---|
| route-enc → encryptRoute(path, opts?) | Promise<string> token |
| route-enc → decryptRoute(token, opts?) | Promise<string \| null>, null on tamper |
| route-enc → buildSecureHref(token, prefix?) | "/x/<token>" |
| route-enc → getDefaultPrefix() | ROUTE_PREFIX env or "/x" |
| route-enc/proxy → createEncryptedProxy({ prefix?, key? }) | Next 16 proxy() handler |
| route-enc/proxy → matcherForPrefix(prefix?) | ["/x/:path*"] helper |
| route-enc/react → <SecureLink href prefix? secureKey?> | Async server component |
opts can be a key string or { key?: string }. Omitted key falls back to ROUTE_ENCRYPTION_KEY.
How it works
encryptRoute("/dashboard")→ random 12-byte IV + AES-GCM ciphertext, base64url-encoded.- User visits
/x/<token>. proxy()decrypts, validates path starts with/(rejects//,://), preserves query string,NextResponse.rewrite()to the real route.- Invalid tokens →
404.
Each encryption uses a fresh IV, so the same path produces a different token every time.
Notes
- Tokens are opaque. Old tokens break if you rotate
ROUTE_ENCRYPTION_KEY— that's expected. config.matchermust be a static string. If you change the prefix, updatematcherby hand.- Client components can't encrypt (no key in browser). Encrypt in a parent server component and pass the href down, or expose a server action that returns an encrypted URL.
Local dev (this repo)
cp .env.example .env.local
npm install
npm run devnpm run build:pkg # build dist/ for publishing
npm run build # build demo Next.js app
npm publish --access public