feat: scaffold gateway worker

This commit is contained in:
2026-06-10 01:32:15 -07:00
commit e70df4ed23
11 changed files with 2205 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.DS_Store
node_modules/
dist/
.wrangler/
.dev.vars
coverage/
*.log

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "cfw-gateway",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"ready": "pnpm typecheck && pnpm test"
},
"dependencies": {
"hono": "^4.8.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260601.0",
"typescript": "^5.8.0",
"vitest": "^3.2.0",
"wrangler": "^4.20.0"
}
}

1862
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

14
src/env.ts Normal file
View File

@@ -0,0 +1,14 @@
export interface FetcherLike {
fetch(request: Request): Promise<Response>;
}
export interface DispatchNamespaceLike {
get(name: string, scriptArgs?: Record<string, unknown>, options?: Record<string, unknown>): FetcherLike;
}
export interface Env {
AUTH: FetcherLike;
DISPATCHER?: DispatchNamespaceLike;
INTERNAL_AUTH_SECRET: string;
DEFAULT_WORKER_NAME: string;
}

63
src/index.ts Normal file
View File

@@ -0,0 +1,63 @@
import { Hono } from "hono";
import type { Env } from "./env";
import type { InternalSession } from "./internal-context";
import { withInternalSession } from "./internal-context";
import { resolveRoute } from "./route-policy";
const app = new Hono<{ Bindings: Env }>();
async function verifySession(request: Request, env: Env): Promise<InternalSession | null> {
const sessionUrl = new URL("/internal/session", request.url);
const sessionRequest = new Request(sessionUrl, {
method: "GET",
headers: request.headers,
});
sessionRequest.headers.set("x-internal-auth-secret", env.INTERNAL_AUTH_SECRET);
const response = await env.AUTH.fetch(sessionRequest);
if (!response.ok) return null;
const session = (await response.json()) as InternalSession;
return session.authenticated ? session : null;
}
app.all("*", async (c) => {
const target = resolveRoute(c.req.raw, c.env);
if (target.kind === "gateway-health") {
return c.json({
ok: true,
service: "cfw-gateway",
});
}
if (target.kind === "auth") {
return c.env.AUTH.fetch(c.req.raw);
}
const session = await verifySession(c.req.raw, c.env);
if (!session) {
return c.json({ error: "unauthorized" }, 401);
}
const request = withInternalSession(c.req.raw, session);
if (!c.env.DISPATCHER) {
return c.json({
ok: true,
routedTo: target.workerName,
userId: session.userId,
});
}
const worker = c.env.DISPATCHER.get(target.workerName, {}, {
limits: {
cpuMs: 20,
subRequests: 20,
},
});
return worker.fetch(request);
});
export default app;

35
src/internal-context.ts Normal file
View File

@@ -0,0 +1,35 @@
export interface InternalSession {
authenticated: boolean;
userId: string | null;
email: string | null;
roles: string[];
source: "better-auth" | "dev-token";
}
const STRIPPED_HEADERS = [
"x-internal-auth-secret",
"x-authenticated-user-id",
"x-authenticated-email",
"x-authenticated-roles",
"x-authenticated-source",
];
export function cloneHeadersWithoutInternalHeaders(headers: Headers): Headers {
const next = new Headers(headers);
for (const header of STRIPPED_HEADERS) {
next.delete(header);
}
return next;
}
export function withInternalSession(request: Request, session: InternalSession): Request {
const headers = cloneHeadersWithoutInternalHeaders(request.headers);
headers.set("x-authenticated-user-id", session.userId ?? "");
headers.set("x-authenticated-email", session.email ?? "");
headers.set("x-authenticated-roles", session.roles.join(","));
headers.set("x-authenticated-source", session.source);
return new Request(request, {
headers,
});
}

23
src/route-policy.ts Normal file
View File

@@ -0,0 +1,23 @@
import type { Env } from "./env";
export type RouteTarget =
| { kind: "auth" }
| { kind: "dynamic-worker"; workerName: string }
| { kind: "gateway-health" };
export function resolveRoute(request: Request, env: Env): RouteTarget {
const url = new URL(request.url);
if (url.pathname === "/healthz") {
return { kind: "gateway-health" };
}
if (url.pathname.startsWith("/api/auth/")) {
return { kind: "auth" };
}
return {
kind: "dynamic-worker",
workerName: env.DEFAULT_WORKER_NAME,
};
}

View File

@@ -0,0 +1,136 @@
import { describe, expect, it, vi } from "vitest";
import worker from "../src/index";
import type { Env, FetcherLike } from "../src/env";
function createAuthService(status: number, body: unknown): FetcherLike {
return {
fetch: vi.fn(async () =>
new Response(JSON.stringify(body), {
status,
headers: {
"content-type": "application/json",
},
}),
),
};
}
describe("cfw-gateway worker", () => {
it("returns health status", async () => {
const env: Env = {
AUTH: createAuthService(401, { authenticated: false }),
INTERNAL_AUTH_SECRET: "dev-internal-secret",
DEFAULT_WORKER_NAME: "customer-worker-1",
};
const response = await worker.fetch(new Request("http://gateway.local/healthz"), env);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
ok: true,
service: "cfw-gateway",
});
});
it("proxies Better Auth routes to the auth service", async () => {
const auth = createAuthService(200, { proxied: true });
const env: Env = {
AUTH: auth,
INTERNAL_AUTH_SECRET: "dev-internal-secret",
DEFAULT_WORKER_NAME: "customer-worker-1",
};
const response = await worker.fetch(new Request("http://gateway.local/api/auth/session"), env);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ proxied: true });
expect(auth.fetch).toHaveBeenCalledOnce();
});
it("rejects non-auth routes when auth service denies the session", async () => {
const env: Env = {
AUTH: createAuthService(401, { authenticated: false }),
INTERNAL_AUTH_SECRET: "dev-internal-secret",
DEFAULT_WORKER_NAME: "customer-worker-1",
};
const response = await worker.fetch(new Request("http://gateway.local/app"), env);
expect(response.status).toBe(401);
await expect(response.json()).resolves.toEqual({ error: "unauthorized" });
});
it("routes authenticated requests to the default worker when dispatcher is absent", async () => {
const env: Env = {
AUTH: createAuthService(200, {
authenticated: true,
userId: "dev-user",
email: "dev@example.local",
roles: ["developer"],
source: "dev-token",
}),
INTERNAL_AUTH_SECRET: "dev-internal-secret",
DEFAULT_WORKER_NAME: "customer-worker-1",
};
const response = await worker.fetch(
new Request("http://gateway.local/app", {
headers: {
authorization: "Bearer dev-session-token",
"x-authenticated-user-id": "spoofed-user",
},
}),
env,
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
ok: true,
routedTo: "customer-worker-1",
userId: "dev-user",
});
});
it("dispatches authenticated requests with trusted identity headers", async () => {
const dispatchedFetch = vi.fn(async (request: Request) => {
return Response.json({
userId: request.headers.get("x-authenticated-user-id"),
email: request.headers.get("x-authenticated-email"),
roles: request.headers.get("x-authenticated-roles"),
spoofedSecret: request.headers.get("x-internal-auth-secret"),
});
});
const env: Env = {
AUTH: createAuthService(200, {
authenticated: true,
userId: "dev-user",
email: "dev@example.local",
roles: ["developer"],
source: "dev-token",
}),
DISPATCHER: {
get: vi.fn(() => ({ fetch: dispatchedFetch })),
},
INTERNAL_AUTH_SECRET: "dev-internal-secret",
DEFAULT_WORKER_NAME: "customer-worker-1",
};
const response = await worker.fetch(
new Request("http://gateway.local/app", {
headers: {
authorization: "Bearer dev-session-token",
"x-internal-auth-secret": "client-spoof",
"x-authenticated-user-id": "client-spoof",
},
}),
env,
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({
userId: "dev-user",
email: "dev@example.local",
roles: "developer",
spoofedSecret: null,
});
expect(dispatchedFetch).toHaveBeenCalledOnce();
});
});

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"types": ["@cloudflare/workers-types", "vitest/globals"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "tests/**/*.ts", "vitest.config.ts"]
}

8
vitest.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: true,
},
});

23
wrangler.jsonc Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cfw-gateway",
"main": "src/index.ts",
"compatibility_date": "2026-06-10",
"compatibility_flags": ["nodejs_compat"],
"vars": {
"INTERNAL_AUTH_SECRET": "dev-internal-secret",
"DEFAULT_WORKER_NAME": "customer-worker-1"
},
"services": [
{
"binding": "AUTH",
"service": "cfw-auth"
}
],
"dispatch_namespaces": [
{
"binding": "DISPATCHER",
"namespace": "platform-workers"
}
]
}