chat-ui-agent-demo / src /lib /server /agent /AgentPreviewCapability.ts
Mike0021's picture
Deploy hardened Agent UX and isolated previews (part 2)
2df2e7e verified
Raw History Blame Contribute Delete
2.92 kB
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { config } from "$lib/server/config";
export const AGENT_PREVIEW_CAPABILITY_TTL_MS = 60 * 60 * 1_000;
const CAPABILITY_PATTERN = /^[A-Za-z0-9_-]{43}$/;
const MIN_RESOLVER_SECRET_BYTES = 32;
export interface IssuedAgentPreviewCapability {
capability: string;
hash: string;
expiresAt: Date;
}
export function issueAgentPreviewCapability(
now = new Date(),
random: (size: number) => Buffer = randomBytes
): IssuedAgentPreviewCapability {
const capability = random(32).toString("base64url");
if (!CAPABILITY_PATTERN.test(capability)) {
throw new Error("Unable to issue a valid Agent preview capability");
}
return {
capability,
hash: hashAgentPreviewCapability(capability),
expiresAt: new Date(now.getTime() + AGENT_PREVIEW_CAPABILITY_TTL_MS),
};
}
export function hashAgentPreviewCapability(capability: string): string {
if (!CAPABILITY_PATTERN.test(capability)) {
throw new Error("Invalid Agent preview capability");
}
return createHash("sha256").update(capability, "utf8").digest("hex");
}
export function agentPreviewOrigin(candidate = config.AGENT_PREVIEW_ORIGIN): string {
let url: URL;
try {
url = new URL(candidate);
} catch {
throw new Error("AGENT_PREVIEW_ORIGIN must be an absolute HTTPS origin");
}
if (
url.protocol !== "https:" ||
url.username ||
url.password ||
url.pathname !== "/" ||
url.search ||
url.hash ||
(candidate !== url.origin && candidate !== `${url.origin}/`)
) {
throw new Error("AGENT_PREVIEW_ORIGIN must be a credential-free HTTPS origin");
}
if (config.PUBLIC_ORIGIN) {
let chatOrigin: string | undefined;
try {
chatOrigin = new URL(config.PUBLIC_ORIGIN).origin;
} catch {
// PUBLIC_ORIGIN is validated by the workspace provisioner. Keep this
// helper's error focused on the preview runtime configuration.
}
if (chatOrigin === url.origin) {
throw new Error("AGENT_PREVIEW_ORIGIN must be distinct from the Chat UI origin");
}
}
return url.origin;
}
export function previewResolverSecret(candidate = config.AGENT_PREVIEW_RESOLVER_SECRET): string {
if (Buffer.byteLength(candidate, "utf8") < MIN_RESOLVER_SECRET_BYTES) {
throw new Error("AGENT_PREVIEW_RESOLVER_SECRET must contain at least 32 bytes");
}
return candidate;
}
export function verifyPreviewResolverAuthorization(
authorization: string | null,
expectedSecret: string
): boolean {
if (!authorization?.startsWith("Bearer ")) return false;
const candidate = authorization.slice("Bearer ".length);
const candidateHash = createHash("sha256").update(candidate, "utf8").digest();
const expectedHash = createHash("sha256").update(expectedSecret, "utf8").digest();
return timingSafeEqual(candidateHash, expectedHash);
}
export function isAgentPreviewCapability(value: unknown): value is string {
return typeof value === "string" && CAPABILITY_PATTERN.test(value);
}