feat: resolve public shortlink redirects

This commit is contained in:
Hermes Agent
2026-06-20 10:39:53 +10:00
parent 4f598225b5
commit d45091a7c3
6 changed files with 450 additions and 3 deletions
+233
View File
@@ -0,0 +1,233 @@
import { describe, expect, it } from 'vitest';
import worker from '../worker/index';
type LinkRow = {
id: string;
alias: string;
scope: 'public' | 'private';
status: 'active' | 'archived' | 'deleted';
link_type: 'redirect' | 'custom';
target_url: string | null;
content_markdown: string | null;
click_count: number;
};
type RunCall = {
sql: string;
params: unknown[];
};
class FakeD1Database {
readonly preparedSql: string[] = [];
readonly runCalls: RunCall[] = [];
constructor(private readonly rows: LinkRow[]) {}
prepare(sql: string): FakeD1PreparedStatement {
this.preparedSql.push(sql);
return new FakeD1PreparedStatement(this, sql);
}
findPublicActive(alias: string): Omit<LinkRow, 'scope' | 'status'> | null {
const row = this.rows.find((candidate) => {
return candidate.alias === alias && candidate.scope === 'public' && candidate.status === 'active';
});
if (!row) {
return null;
}
return {
id: row.id,
alias: row.alias,
link_type: row.link_type,
target_url: row.target_url,
content_markdown: row.content_markdown,
click_count: row.click_count,
};
}
}
class FakeD1PreparedStatement {
constructor(
private readonly db: FakeD1Database,
private readonly sql: string,
private readonly params: unknown[] = [],
) {}
bind(...params: unknown[]): FakeD1PreparedStatement {
return new FakeD1PreparedStatement(this.db, this.sql, params);
}
async first<T>(): Promise<T | null> {
expect(this.sql).toContain("scope='public'");
expect(this.sql).toContain("status='active'");
expect(this.sql).toContain('alias=?');
const alias = String(this.params[0]);
return this.db.findPublicActive(alias) as T | null;
}
async run(): Promise<D1Result> {
this.db.runCalls.push({ sql: this.sql, params: this.params });
return { success: true, meta: {} } as D1Result;
}
}
class FakeExecutionContext {
readonly promises: Promise<unknown>[] = [];
waitUntil(promise: Promise<unknown>): void {
this.promises.push(promise);
}
passThroughOnException(): void {}
}
function makeEnv(rows: LinkRow[] = []) {
const db = new FakeD1Database(rows);
return { env: { DB: db as unknown as D1Database }, db };
}
async function fetchWorker(url: string, rows: LinkRow[] = []) {
const { env, db } = makeEnv(rows);
const ctx = new FakeExecutionContext();
const response = await worker.fetch(
new Request(url) as unknown as Parameters<typeof worker.fetch>[0],
env as unknown as Parameters<typeof worker.fetch>[1],
ctx as unknown as Parameters<typeof worker.fetch>[2],
);
return { response, db, ctx };
}
describe('public heygo.cc shortlink redirects', () => {
it('redirects a public redirect link and records analytics asynchronously', async () => {
const { response, ctx, db } = await fetchWorker('https://heygo.cc/docs', [
{
id: 'link_1',
alias: 'docs',
scope: 'public',
status: 'active',
link_type: 'redirect',
target_url: 'https://example.com/docs',
content_markdown: null,
click_count: 0,
},
]);
expect(response.status).toBe(302);
expect(response.headers.get('location')).toBe('https://example.com/docs');
expect(ctx.promises).toHaveLength(1);
await Promise.all(ctx.promises);
expect(db.runCalls).toHaveLength(1);
expect(db.runCalls[0].sql).toContain('UPDATE links SET click_count = click_count + 1');
expect(db.runCalls[0].params).toEqual(['link_1']);
});
it('renders a public custom link as escaped HTML', async () => {
const { response } = await fetchWorker('https://heygo.cc/about', [
{
id: 'link_2',
alias: 'about',
scope: 'public',
status: 'active',
link_type: 'custom',
target_url: null,
content_markdown: '# About Heygo\n<script>alert("x")</script>',
click_count: 5,
},
]);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('text/html');
const html = await response.text();
expect(html).toContain('About Heygo');
expect(html).not.toContain('<script>');
expect(html).toContain('&lt;script&gt;');
});
it('returns a public 404 HTML response for an unknown alias on heygo.cc', async () => {
const { response } = await fetchWorker('https://heygo.cc/missing');
expect(response.status).toBe(404);
expect(response.headers.get('content-type')).toContain('text/html');
await expect(response.text()).resolves.toContain('Not found');
});
it('resolves template parameters from query, pathParam, and defaults', async () => {
const { response } = await fetchWorker('https://heygo.cc/search/path%20value?lang=fr', [
{
id: 'link_3',
alias: 'search',
scope: 'public',
status: 'active',
link_type: 'redirect',
target_url: 'https://search.example/{query,default=hello}?lang={lang,default=en}&page={page,default=1}',
content_markdown: null,
click_count: 0,
},
]);
expect(response.status).toBe(302);
expect(response.headers.get('location')).toBe('https://search.example/path%20value?lang=fr&page=1');
});
it('does not return private links through the heygo.cc public route', async () => {
const { response } = await fetchWorker('https://heygo.cc/secret', [
{
id: 'link_4',
alias: 'secret',
scope: 'private',
status: 'active',
link_type: 'redirect',
target_url: 'https://example.com/secret',
content_markdown: null,
click_count: 0,
},
]);
expect(response.status).toBe(404);
});
it('keeps /api/health working and does not treat it as an alias', async () => {
const { response, db } = await fetchWorker('https://heygo.cc/api/health', [
{
id: 'link_5',
alias: 'api',
scope: 'public',
status: 'active',
link_type: 'redirect',
target_url: 'https://example.com/api',
content_markdown: null,
click_count: 0,
},
]);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ ok: true, service: 'heygo-worker' });
expect(db.preparedSql).toHaveLength(0);
});
it('reserves /app/* and /admin/* without D1 alias lookup', async () => {
for (const path of ['/app/foo', '/admin/foo']) {
const { response, db } = await fetchWorker(`https://heygo.cc${path}`, [
{
id: `link_${path}`,
alias: path.split('/')[1],
scope: 'public',
status: 'active',
link_type: 'redirect',
target_url: 'https://example.com/reserved',
content_markdown: null,
click_count: 0,
},
]);
expect(response.status).toBe(404);
expect(db.preparedSql).toHaveLength(0);
}
});
});
+12 -1
View File
@@ -1,10 +1,21 @@
import { describe, expect, it } from 'vitest';
import worker from '../worker/index';
class FakeExecutionContext {
waitUntil(): void {}
passThroughOnException(): void {}
}
function fetchWorker(path: string) {
const request = new Request(`https://heygo.test${path}`);
const env = { DB: {} as D1Database };
const ctx = new FakeExecutionContext();
return worker.fetch(request as unknown as Parameters<typeof worker.fetch>[0]);
return worker.fetch(
request as unknown as Parameters<typeof worker.fetch>[0],
env as unknown as Parameters<typeof worker.fetch>[1],
ctx as unknown as Parameters<typeof worker.fetch>[2],
);
}
describe('worker API scaffold', () => {
+5
View File
@@ -0,0 +1,5 @@
export interface Env {
DB: D1Database;
PUBLIC_LINK_CACHE?: KVNamespace;
ASSETS?: Fetcher;
}
+9 -2
View File
@@ -1,4 +1,7 @@
export interface Env {}
import type { Env } from './env';
import { handlePublicShortlink, isHeygoPublicHost, isReservedPublicPath } from './routes/redirect';
export type { Env } from './env';
const jsonHeaders = {
'content-type': 'application/json; charset=utf-8',
@@ -15,7 +18,7 @@ function json(body: unknown, init: ResponseInit = {}) {
}
export default {
async fetch(request): Promise<Response> {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/health') {
@@ -26,6 +29,10 @@ export default {
return json({ error: 'Not found' }, { status: 404 });
}
if (isHeygoPublicHost(url) && !isReservedPublicPath(url.pathname)) {
return handlePublicShortlink(request, env, ctx);
}
return json({ error: 'Not found' }, { status: 404 });
},
} satisfies ExportedHandler<Env>;
+32
View File
@@ -0,0 +1,32 @@
export function htmlResponse(body: string, init: ResponseInit = {}): Response {
return new Response(body, {
...init,
headers: {
'content-type': 'text/html; charset=utf-8',
...init.headers,
},
});
}
export function publicNotFoundResponse(): Response {
return htmlResponse(
'<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Not found</title></head><body><h1>Not found</h1><p>This short link does not exist.</p></body></html>',
{ status: 404 },
);
}
export function publicBadRequestResponse(message = 'Invalid short link'): Response {
return htmlResponse(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Invalid short link</title></head><body><h1>Invalid short link</h1><p>${escapeHtml(message)}</p></body></html>`,
{ status: 400 },
);
}
export function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
+159
View File
@@ -0,0 +1,159 @@
import type { Env } from '../env';
import { validateAlias } from '../lib/aliases';
import { escapeHtml, htmlResponse, publicBadRequestResponse, publicNotFoundResponse } from '../lib/responses';
import { resolveTemplateUrl, TemplateResolutionError } from '../lib/templates';
type PublicLinkRow = {
id: string;
alias: string;
link_type: 'redirect' | 'custom';
target_url: string | null;
content_markdown: string | null;
click_count: number;
};
const PUBLIC_LINK_QUERY = `SELECT id, alias, link_type, target_url, content_markdown, click_count
FROM links
WHERE scope='public' AND status='active' AND alias=?
LIMIT 1`;
const CLICK_COUNT_UPDATE = `UPDATE links SET click_count = click_count + 1, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id=?`;
export function isReservedPublicPath(pathname: string): boolean {
return pathname === '/api' || pathname.startsWith('/api/')
|| pathname === '/app' || pathname.startsWith('/app/')
|| pathname === '/admin' || pathname.startsWith('/admin/');
}
export function isHeygoPublicHost(url: URL): boolean {
return url.hostname === 'heygo.cc';
}
export async function handlePublicShortlink(
request: Request,
env: Env,
ctx: Pick<ExecutionContext, 'waitUntil'>,
): Promise<Response> {
const url = new URL(request.url);
const pathParts = parseAliasPath(url.pathname);
if (!pathParts) {
return publicNotFoundResponse();
}
const aliasValidation = validateAlias(pathParts.alias);
if (!aliasValidation.ok || aliasValidation.value !== pathParts.alias.toLowerCase()) {
return publicNotFoundResponse();
}
const link = await env.DB.prepare(PUBLIC_LINK_QUERY).bind(aliasValidation.value).first<PublicLinkRow>();
if (!link) {
return publicNotFoundResponse();
}
ctx.waitUntil(recordClick(env.DB, link.id));
if (link.link_type === 'redirect') {
if (!link.target_url) {
return publicNotFoundResponse();
}
try {
const location = resolveTemplateUrl({
targetUrl: link.target_url,
pathParam: pathParts.pathParam,
query: url.searchParams,
});
return Response.redirect(location, 302);
} catch (error) {
if (error instanceof TemplateResolutionError) {
return publicBadRequestResponse(error.message);
}
throw error;
}
}
if (link.link_type === 'custom') {
return htmlResponse(renderCustomLink(link), { status: 200 });
}
return publicNotFoundResponse();
}
type ParsedAliasPath = {
alias: string;
pathParam?: string;
};
function parseAliasPath(pathname: string): ParsedAliasPath | null {
const rawSegments = pathname.split('/').slice(1);
if (rawSegments.length === 0 || rawSegments.length > 2 || rawSegments[0] === '') {
return null;
}
if (rawSegments.length === 2 && rawSegments[1] === '') {
return null;
}
try {
const alias = decodeURIComponent(rawSegments[0]);
const pathParam = rawSegments[1] === undefined ? undefined : decodeURIComponent(rawSegments[1]);
return { alias, pathParam };
} catch {
return null;
}
}
async function recordClick(db: D1Database, linkId: string): Promise<void> {
try {
await db.prepare(CLICK_COUNT_UPDATE).bind(linkId).run();
} catch {
// Analytics must never block or break a public redirect/render.
}
}
function renderCustomLink(link: PublicLinkRow): string {
const content = renderMarkdown(link.content_markdown ?? '');
const title = escapeHtml(link.alias);
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title></head><body><main>${content}</main></body></html>`;
}
function renderMarkdown(markdown: string): string {
const lines = markdown.replace(/\r\n?/g, '\n').split('\n');
const rendered: string[] = [];
let paragraph: string[] = [];
const flushParagraph = () => {
if (paragraph.length > 0) {
rendered.push(`<p>${paragraph.join('<br>')}</p>`);
paragraph = [];
}
};
for (const line of lines) {
const escaped = escapeHtml(line);
const heading = escaped.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
flushParagraph();
const level = heading[1].length;
rendered.push(`<h${level}>${heading[2]}</h${level}>`);
continue;
}
if (escaped.trim() === '') {
flushParagraph();
continue;
}
paragraph.push(escaped);
}
flushParagraph();
return rendered.join('\n');
}