File size: 2,923 Bytes
2df2e7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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);
}