token-server.ts
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import {
readSigningKey,
signUserToken,
type Audience,
type SigningKey,
} from "./user-token.ts";
type SignedInUser = Readonly<{
id: string;
name: string;
}>;
const DEMO_VISITORS: ReadonlyMap<string, SignedInUser> = new Map([
["demo-visitor", { id: "user-1042", name: "Marta" }],
]);
const DEMO_OWNERS: ReadonlyMap<string, SignedInUser> = new Map([
["demo-owner", { id: "owner-lena", name: "Lena" }],
]);
function cookie(request: IncomingMessage, name: string): string | undefined {
for (const part of (request.headers.cookie ?? "").split(";")) {
const [key, ...value] = part.trim().split("=");
if (key === name) return value.join("=");
}
return undefined;
}
function currentVisitor(
request: IncomingMessage,
demoUsers: boolean,
): SignedInUser | undefined {
if (!demoUsers) return undefined;
const session = cookie(request, "session");
return session === undefined ? undefined : DEMO_VISITORS.get(session);
}
function currentOwner(
request: IncomingMessage,
demoUsers: boolean,
): SignedInUser | undefined {
if (!demoUsers) return undefined;
const session = cookie(request, "session");
return session === undefined ? undefined : DEMO_OWNERS.get(session);
}
type Route = Readonly<{
label: string;
path: string;
audience: Audience;
currentUser: (
request: IncomingMessage,
demoUsers: boolean,
) => SignedInUser | undefined;
}>;
const SITE_ROUTE: Route = {
label: "your site's route",
path: "/messenger/token",
audience: "messenger-widget",
currentUser: currentVisitor,
};
const DASHBOARD_ROUTE: Route = {
label: "your owner dashboard's route",
path: "/messenger/inbox-token",
audience: "messenger-inbox",
currentUser: currentOwner,
};
type Config = Readonly<{
publishableKey: string;
key: SigningKey;
sitePort: number;
dashboardPort: number;
demoUsers: boolean;
}>;
function required(name: string): string {
const value = process.env[name];
if (value === undefined || value === "")
throw new Error(
`${name} is not set. See the comment at the top of token-server.ts.`,
);
return value;
}
function readPort(name: string, fallback: number): number {
const value = Number.parseInt(process.env[name] ?? String(fallback), 10);
if (!Number.isInteger(value) || value < 0 || value > 65_535)
throw new Error(`${name} must be a port number.`);
return value;
}
function readConfig(): Config {
const publishableKey = required("MESSENGER_PUBLISHABLE_KEY");
const demoUsers = process.env["EXAMPLE_DEMO_USERS"] === "1";
if (demoUsers && !publishableKey.startsWith("pk_test_"))
throw new Error(
"EXAMPLE_DEMO_USERS=1 works only with a sandbox site's publishable key, pk_test_..., since its invented sign-ins let anyone in.",
);
return {
publishableKey,
key: readSigningKey(
required("MESSENGER_TOKEN_KEY_FILE"),
required("MESSENGER_TOKEN_KID"),
),
sitePort: readPort("PORT", 8787),
dashboardPort: readPort("DASHBOARD_PORT", 8788),
demoUsers,
};
}
function answer(
response: ServerResponse,
status: number,
body = "",
headers: Readonly<Record<string, string>> = {},
): void {
response.writeHead(status, {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-store",
...headers,
});
response.end(body);
}
function handle(
config: Config,
route: Route,
request: IncomingMessage,
response: ServerResponse,
): void {
const path = new URL(request.url ?? "/", "http://localhost").pathname;
if (path !== route.path) return answer(response, 404);
if (request.method !== "POST")
return answer(response, 405, "", { Allow: "POST" });
const site = request.headers["sec-fetch-site"];
if (site !== undefined && site !== "same-origin")
return answer(response, 403);
const user = route.currentUser(request, config.demoUsers);
if (user === undefined) return answer(response, 204);
const token = signUserToken(config.key, {
publishableKey: config.publishableKey,
audience: route.audience,
subject: user.id,
name: user.name,
});
answer(response, 200, token);
}
function serve(config: Config, route: Route, port: number): void {
const server = createServer((request, response) => {
try {
handle(config, route, request, response);
} catch {
answer(response, 500);
}
});
server.on("error", (error) => {
console.error(`token-server: ${route.label}: ${error.message}`);
process.exit(1);
});
server.listen(port, "127.0.0.1", () => {
const address = server.address();
const bound =
typeof address === "object" && address !== null ? address.port : port;
console.log(
`token-server: ${route.label} on http://localhost:${String(bound)}${route.path}`,
);
});
}
function main(): void {
let config: Config;
try {
config = readConfig();
} catch (error) {
console.error(
`token-server: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
serve(config, SITE_ROUTE, config.sitePort);
serve(config, DASHBOARD_ROUTE, config.dashboardPort);
if (!config.demoUsers)
console.log(
"token-server: no sign-in wired in, so every request answers 204: nobody signed in. Replace currentVisitor() and currentOwner().",
);
}
main();