Files

231 lines
7.0 KiB
TypeScript

// Vendored from @openai/sites-vite-plugin 0.2.0 (openai/sites#9).
// See sites-vite-plugin.LICENSE for the upstream MIT license.
import { access, cp, mkdir, rm } from "node:fs/promises";
import type { IncomingMessage, ServerResponse } from "node:http";
import { resolve } from "node:path";
import type { Plugin } from "vite";
const localUserId = "local_seedy";
const localEmail = "seedy@sites.test";
const localFullName = "Seedy";
const localCookieName = "__sites_local_auth";
const localHosts = new Set(["localhost", "127.0.0.1", "::1"]);
const localAddresses = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
const authPaths = new Set([
"/signin-with-chatgpt",
"/signout-with-chatgpt",
"/callback",
]);
async function exists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
}
throw error;
}
}
export function sites({ mockAuth = true } = {}): Plugin {
let root = process.cwd();
let command: "build" | "serve" = "build";
return {
name: "sites",
configResolved(config) {
root = config.root;
command = config.command;
},
configureServer(server) {
if (!mockAuth) return;
const secure = Boolean(server.config.server.https);
server.config.logger.info(`Sites local sign-in: ${localEmail}`);
server.middlewares.use((request, response, next) => {
for (const name of Object.keys(request.headers)) {
if (name.startsWith("oai-authenticated-user-")) {
removeHeader(request, name);
}
}
let authority: URL;
let url: URL;
try {
authority = new URL(
`${secure ? "https" : "http"}://${request.headers.host}`,
);
url = new URL(request.url ?? "/", authority);
} catch {
if (authPaths.has((request.url ?? "/").split("?")[0])) {
respond(response, 403);
} else {
next();
}
return;
}
const hostname = authority.hostname
.replace(/^\[|\]$/g, "")
.toLowerCase();
if (
!localHosts.has(hostname) ||
!localAddresses.has(request.socket.remoteAddress ?? "") ||
url.origin !== authority.origin
) {
if (authPaths.has(url.pathname)) respond(response, 403);
else next();
return;
}
const cookies = (request.headers.cookie ?? "")
.split(";")
.map((cookie) => cookie.trim())
.filter(Boolean);
const signInCookies = cookies
.filter((cookie) => cookie.startsWith(`${localCookieName}=`))
.map((cookie) => cookie.slice(localCookieName.length + 1));
const applicationCookies = cookies.filter(
(cookie) => !cookie.startsWith(`${localCookieName}=`),
);
if (applicationCookies.length !== cookies.length) {
removeHeader(request, "cookie");
if (applicationCookies.length) {
setHeader(request, "cookie", applicationCookies.join("; "));
}
}
if (url.pathname === "/callback") {
respond(response, 501);
return;
}
const signIn = url.pathname === "/signin-with-chatgpt";
const signOut = url.pathname === "/signout-with-chatgpt";
if (!signIn && !signOut) {
if (signInCookies.length === 1 && signInCookies[0] === "1") {
setHeader(request, "oai-authenticated-user-id", localUserId);
setHeader(request, "oai-authenticated-user-email", localEmail);
setHeader(
request,
"oai-authenticated-user-full-name",
localFullName,
);
setHeader(
request,
"oai-authenticated-user-full-name-encoding",
"percent-encoded-utf-8",
);
}
next();
return;
}
if (
(request.headers.origin && request.headers.origin !== url.origin) ||
request.headers["sec-fetch-site"] === "cross-site"
) {
respond(response, 403);
return;
}
if (
request.headers["next-router-prefetch"] !== undefined ||
request.headers["x-middleware-prefetch"] === "1" ||
[request.headers.purpose, request.headers["sec-purpose"]].some(
(value) =>
typeof value === "string" &&
value
.split(/[;,]/)
.some((part) => part.trim().toLowerCase() === "prefetch"),
)
) {
respond(response, 204);
return;
}
if (
request.method !== "GET" &&
(!signOut || request.method !== "POST")
) {
response.setHeader("Allow", signIn ? "GET" : "GET, POST");
respond(response, 405);
return;
}
response.statusCode = request.method === "POST" ? 303 : 302;
response.setHeader("Cache-Control", "private, no-store");
response.setHeader(
"Location",
safeReturn(url.searchParams.get("return_to")),
);
response.setHeader(
"Set-Cookie",
`${localCookieName}=${signIn ? "1" : ""}; Path=/; ${
signOut ? "Max-Age=0; " : ""
}HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`,
);
response.end();
});
},
async closeBundle() {
if (command !== "build") return;
const outputDirectory = resolve(root, "dist", ".openai");
const hostingConfig = resolve(root, ".openai", "hosting.json");
const drizzleSource = resolve(root, "drizzle");
await rm(outputDirectory, { recursive: true, force: true });
await mkdir(outputDirectory, { recursive: true });
await cp(hostingConfig, resolve(outputDirectory, "hosting.json"));
if (await exists(drizzleSource)) {
await cp(drizzleSource, resolve(outputDirectory, "drizzle"), {
recursive: true,
});
}
},
};
}
function removeHeader(request: IncomingMessage, name: string): void {
delete request.headers[name];
for (let index = request.rawHeaders.length - 2; index >= 0; index -= 2) {
if (request.rawHeaders[index]?.toLowerCase() === name) {
request.rawHeaders.splice(index, 2);
}
}
}
function setHeader(
request: IncomingMessage,
name: string,
value: string,
): void {
removeHeader(request, name);
request.headers[name] = value;
request.rawHeaders.push(name, value);
}
function respond(response: ServerResponse, status: number): void {
response.statusCode = status;
response.setHeader("Cache-Control", "private, no-store");
response.end();
}
function safeReturn(value: string | null): string {
if (!value?.startsWith("/") || value.startsWith("//")) return "/";
try {
const url = new URL(value, "http://localhost");
if (url.origin !== "http://localhost" || authPaths.has(url.pathname)) {
return "/";
}
return `${url.pathname}${url.search}${url.hash}`;
} catch {
return "/";
}
}