feat: add terraform IaC, dev/prod environments, and multi-host worker support

- Terraform configs for D1 databases, KV namespaces, Worker custom domains
- wrangler.dev.jsonc and wrangler.prod.jsonc for environment-specific deployments
- Worker code refactored to use env vars for host checking (PUBLIC_HOST, PRIVATE_HOST)
- Configurable app URLs and cookie domain via env vars
- Deploy and migrate npm scripts for dev/prod
- Updated all tests with new env fixtures
- Deployment guide in README
This commit is contained in:
Hermes Agent
2026-06-20 14:26:56 +10:00
parent f4bf43189f
commit e4fbbaec14
28 changed files with 675 additions and 48 deletions
+10 -8
View File
@@ -29,21 +29,23 @@ export function isAuthProviderEnabled(provider: string): provider is EnabledAuth
return ENABLED_PROVIDER_SET.has(provider);
}
export function getSessionCookieAttributes(requestUrl: string | URL): string[] {
const url = typeof requestUrl === 'string' ? new URL(requestUrl) : requestUrl;
export function getSessionCookieAttributes(
requestUrl: string | URL,
cookieDomain: string,
): string[] {
// The request URL is retained so callers can derive a host-specific domain in
// the future; the shared Domain attribute now comes from the COOKIE_DOMAIN
// env var so each deployment (dev/prod/local) controls its own scope.
void requestUrl;
const attributes = ['HttpOnly', 'Secure', 'SameSite=Lax', 'Path=/'];
if (isHeygoProductionHost(url.hostname)) {
attributes.push('Domain=.heygo.cc');
if (cookieDomain) {
attributes.push(`Domain=${cookieDomain}`);
}
return attributes;
}
function isHeygoProductionHost(hostname: string): boolean {
return hostname === 'heygo.cc' || hostname.endsWith('.heygo.cc');
}
export class AuthError extends Error {
readonly status: number;
+4
View File
@@ -2,4 +2,8 @@ export interface Env {
DB: D1Database;
PUBLIC_LINK_CACHE?: KVNamespace;
ASSETS?: Fetcher;
PUBLIC_HOST: string;
PRIVATE_HOST: string;
APP_BASE_URL: string;
COOKIE_DOMAIN: string;
}
+9 -9
View File
@@ -21,8 +21,8 @@ function json(body: unknown, init: ResponseInit = {}) {
});
}
function withPrivateHostNoStoreHeaders(url: URL, response: Response): Response {
return isHeygoPrivateHost(url) ? withPrivateNoStoreHeaders(response) : response;
function withPrivateHostNoStoreHeaders(url: URL, response: Response, privateHost: string): Response {
return isHeygoPrivateHost(url.hostname, privateHost) ? withPrivateNoStoreHeaders(response) : response;
}
export default {
@@ -30,29 +30,29 @@ export default {
const url = new URL(request.url);
if (url.pathname === '/api/health') {
return withPrivateHostNoStoreHeaders(url, json({ ok: true, service: 'heygo-worker' }));
return withPrivateHostNoStoreHeaders(url, json({ ok: true, service: 'heygo-worker' }), env.PRIVATE_HOST);
}
if (url.pathname.startsWith('/api/')) {
const apiResponse = await handleLinksApi(request, env);
if (apiResponse) {
return withPrivateHostNoStoreHeaders(url, apiResponse);
return withPrivateHostNoStoreHeaders(url, apiResponse, env.PRIVATE_HOST);
}
const promotionsResponse = await handlePromotionsApi(request, env);
if (promotionsResponse) {
return withPrivateHostNoStoreHeaders(url, promotionsResponse);
return withPrivateHostNoStoreHeaders(url, promotionsResponse, env.PRIVATE_HOST);
}
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }));
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }), env.PRIVATE_HOST);
}
if (isHeygoPublicHost(url) && !isReservedPublicPath(url.pathname)) {
if (isHeygoPublicHost(url.hostname, env.PUBLIC_HOST) && !isReservedPublicPath(url.pathname)) {
return handlePublicShortlink(request, env, ctx);
}
if (isHeygoPrivateHost(url) && !isReservedPublicPath(url.pathname)) {
if (isHeygoPrivateHost(url.hostname, env.PRIVATE_HOST) && !isReservedPublicPath(url.pathname)) {
return handlePrivateShortlink(request, env, ctx);
}
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }));
return withPrivateHostNoStoreHeaders(url, json({ error: 'Not found' }, { status: 404 }), env.PRIVATE_HOST);
},
} satisfies ExportedHandler<Env>;
+20 -6
View File
@@ -61,19 +61,33 @@ export function publicBadRequestResponse(message = 'Invalid short link'): Respon
);
}
export const PRIVATE_LOGIN_URL = 'https://heygo.cc/app/login';
export const PRIVATE_APP_URL = 'https://heygo.cc/app/private';
/**
* Build the app login URL for a given environment's APP_BASE_URL.
* Kept as a function (not a constant) so each deployment's base URL is honored.
*/
export function privateLoginUrl(appBaseUrl: string): string {
return `${appBaseUrl}/app/login`;
}
export function privateLoginRequiredResponse(): Response {
/**
* Build the private links app URL for a given environment's APP_BASE_URL.
*/
export function privateAppUrl(appBaseUrl: string): string {
return `${appBaseUrl}/app/private`;
}
export function privateLoginRequiredResponse(appBaseUrl: string): Response {
const loginUrl = privateLoginUrl(appBaseUrl);
return privateHtmlResponse(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Login required</title></head><body><h1>Not found</h1><p><a href="${PRIVATE_LOGIN_URL}">Login to use your private links</a>.</p></body></html>`,
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Login required</title></head><body><h1>Not found</h1><p><a href="${loginUrl}">Login to use your private links</a>.</p></body></html>`,
{ status: 404 },
);
}
export function privateAliasNotFoundResponse(): Response {
export function privateAliasNotFoundResponse(appBaseUrl: string): Response {
const appUrl = privateAppUrl(appBaseUrl);
return privateHtmlResponse(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Not found</title></head><body><h1>Not found</h1><p>This private link does not exist.</p><p><a href="${PRIVATE_APP_URL}">Create this private link</a>.</p></body></html>`,
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Not found</title></head><body><h1>Not found</h1><p>This private link does not exist.</p><p><a href="${appUrl}">Create this private link</a>.</p></body></html>`,
{ status: 404 },
);
}
+11 -11
View File
@@ -3,8 +3,8 @@ import { getCurrentUser } from '../auth';
import { validateAlias } from '../lib/aliases';
import { renderCustomLinkHtml } from '../lib/custom-link';
import {
PRIVATE_APP_URL,
privateAliasNotFoundResponse,
privateAppUrl,
privateHtmlResponse,
privateLoginRequiredResponse,
privateRedirectResponse,
@@ -30,8 +30,8 @@ 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 isHeygoPrivateHost(url: URL): boolean {
return url.hostname === 'my.heygo.cc';
export function isHeygoPrivateHost(hostname: string, privateHost: string): boolean {
return hostname === privateHost;
}
export async function handlePrivateShortlink(
@@ -45,9 +45,9 @@ export async function handlePrivateShortlink(
if (url.pathname === '/') {
const user = await getCurrentUser(request, env);
if (!user) {
return privateLoginRequiredResponse();
return privateLoginRequiredResponse(env.APP_BASE_URL);
}
return privateRedirectResponse(PRIVATE_APP_URL, 302);
return privateRedirectResponse(privateAppUrl(env.APP_BASE_URL), 302);
}
const pathParts = parseAliasPath(url.pathname);
@@ -62,21 +62,21 @@ export async function handlePrivateShortlink(
const user = await getCurrentUser(request, env);
if (!user) {
return privateLoginRequiredResponse();
return privateLoginRequiredResponse(env.APP_BASE_URL);
}
const link = await env.DB.prepare(PRIVATE_LINK_QUERY)
.bind(user.id, aliasValidation.value)
.first<PrivateLinkRow>();
if (!link) {
return privateAliasNotFoundResponse();
return privateAliasNotFoundResponse(env.APP_BASE_URL);
}
ctx.waitUntil(recordClick(env.DB, link.id));
if (link.link_type === 'redirect') {
if (!link.target_url) {
return privateAliasNotFoundResponse();
return privateAliasNotFoundResponse(env.APP_BASE_URL);
}
try {
@@ -100,15 +100,15 @@ export async function handlePrivateShortlink(
return privateHtmlResponse(renderCustomLinkHtml(link), { status: 200 });
}
return privateAliasNotFoundResponse();
return privateAliasNotFoundResponse(env.APP_BASE_URL);
}
async function privateNotFoundOrLogin(request: Request, env: Env): Promise<Response> {
const user = await getCurrentUser(request, env);
if (!user) {
return privateLoginRequiredResponse();
return privateLoginRequiredResponse(env.APP_BASE_URL);
}
return privateAliasNotFoundResponse();
return privateAliasNotFoundResponse(env.APP_BASE_URL);
}
async function recordClick(db: D1Database, linkId: string): Promise<void> {
+2 -2
View File
@@ -26,8 +26,8 @@ export function isReservedPublicPath(pathname: string): boolean {
|| pathname === '/admin' || pathname.startsWith('/admin/');
}
export function isHeygoPublicHost(url: URL): boolean {
return url.hostname === 'heygo.cc';
export function isHeygoPublicHost(hostname: string, publicHost: string): boolean {
return hostname === publicHost;
}
export async function handlePublicShortlink(