231 lines
7.0 KiB
TypeScript
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 "/";
|
|
}
|
|
}
|