import { ModelError } from "./model-runtime";
import type { IncomingHttpHeaders } from "node:http";
export function hostingConfig(env: NodeJS.ProcessEnv = process.env) {
const hosted = env.CONTEXT_HOSTED === "true";
const port = Number(env.PORT || env.CONTEXT_PORT || 4191);
if (!Number.isInteger(port) || port < 1 || port > 65535)
throw new Error("Invalid server port.");
const origins = (env.CONTEXT_ALLOWED_ORIGINS || "")
.split(",")
.map((x) => x.trim())
.filter(Boolean);
const reviewers = (env.CONTEXT_REVIEWER_IDS || "")
.split(",")
.map((x) => x.trim())
.filter(Boolean);
if (hosted) {
if (
env.CONTEXT_AUTH_PROVIDER !== "azure-easyauth" ||
!env.WEBSITE_SITE_NAME ||
!reviewers.length ||
!origins.length
)
throw new Error(
"Hosted preview requires Azure App Service authentication, reviewer IDs and explicit HTTPS origins."
);
for (const origin of origins) {
const url = new URL(origin);
if (
url.origin !== origin ||
url.protocol !== "https:" ||
url.username ||
url.password
)
throw new Error("Hosted origins must be exact HTTPS origins.");
}
if (!reviewers.every((id) => /^[a-f0-9-]{36}$/i.test(id)))
throw new Error("Reviewer IDs must be Azure principal IDs.");
}
return {
hosted,
port,
origins,
reviewers,
bind: hosted ? "0.0.0.0" : "127.0.0.1",
};
}
export type HostingConfig = ReturnType<typeof hostingConfig>;
export function requestOwner(
config: HostingConfig,
headers: IncomingHttpHeaders
) {
const host = headers.host || "";
const origin = headers.origin;
if (!config.hosted) {
const allowed = new RegExp(
`^(127\\.0\\.0\\.1|localhost):(?:${config.port}|4190)$`
);
if (!allowed.test(host)) throw new ModelError("Local demo only.", 403);
if (
origin &&
(!origin.startsWith("http://") || !allowed.test(origin.slice(7)))
)
throw new ModelError("Origin is not allowed.", 403);
return "local";
}
if (
!config.origins.some((value) => new URL(value).host === host) ||
(origin && !config.origins.includes(origin))
)
throw new ModelError("Origin or host is not allowed.", 403);
// Azure App Service Authentication must be enabled with authentication required.
// App Service injects these headers; never expose this process outside its proxy.
const principal = headers["x-ms-client-principal-id"];
if (typeof principal !== "string" || !config.reviewers.includes(principal))
throw new ModelError(
"This preview is restricted to approved reviewers. Sign in with your invited account.",
401
);
return principal;
}
export function createPreviewLimiter() {
const windows = new Map<
string,
{ minute: number; count: number; day: number; daily: number }
>();
return (owner: string, now = Date.now()) => {
const minute = Math.floor(now / 60_000),
day = Math.floor(now / 86_400_000);
for (const [id, item] of windows) if (item.day !== day) windows.delete(id);
const value = windows.get(owner) || { minute, count: 0, day, daily: 0 };
if (value.minute !== minute) {
value.minute = minute;
value.count = 0;
}
if (value.count >= 6 || value.daily >= 100)
throw new ModelError(
"Preview extraction limit reached. Your saved work is unchanged. Try again later.",
429,
value.count >= 6 ? 60 : 3600
);
value.count++;
value.daily++;
windows.set(owner, value);
};
}