Repository navigation
Expand file tree
/
Copy pathsecurity.ts
More file actions
87 lines (77 loc) · 2.94 KB
/
Copy pathsecurity.ts
File metadata and controls
87 lines (77 loc) · 2.94 KB
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
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
function usableSecret(value: unknown): string {
const trimmed = typeof value === "string" ? value.trim() : "";
if (!trimmed || (trimmed.startsWith("${") && trimmed.endsWith("}"))) return "";
return trimmed;
}
function readAgentmemoryFile(name: string): string {
try {
return readFileSync(join(homedir(), ".agentmemory", name), "utf-8");
} catch {
return "";
}
}
function envFileSecret(): string {
let found = "";
for (const line of readAgentmemoryFile(".env").split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
if (trimmed.slice(0, eq).replace(/^export\s+/, "").trim() !== "AGENTMEMORY_SECRET") continue;
let value = trimmed.slice(eq + 1).trim();
const quote = value[0];
const close = quote === '"' || quote === "'" ? value.indexOf(quote, 1) : -1;
if (close > 0) value = value.slice(1, close);
else if (value.includes(" #")) value = value.slice(0, value.indexOf(" #"));
found = usableSecret(value);
}
return found;
}
function isLoopbackUrl(url: string): boolean {
try {
const host = new URL(url).hostname.replace(/^\[|\]$/g, "").toLowerCase();
return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
} catch {
return false;
}
}
export function resolveSecret(url: string, explicit?: string): string {
const configured = usableSecret(explicit);
if (configured) return configured;
if (!isLoopbackUrl(url)) return "";
return envFileSecret() || usableSecret(readAgentmemoryFile("secret"));
}
function normalizedHostname(hostname: string): string {
return hostname.replace(/^\[|\]$/g, "").toLowerCase();
}
export function usesPlaintextBearerAuth(baseUrl: string, secret?: string): boolean {
if (!secret) return false;
try {
const parsed = new URL(baseUrl);
return parsed.protocol === "http:" && !LOOPBACK_HOSTS.has(normalizedHostname(parsed.hostname));
} catch {
return false;
}
}
export function plaintextBearerAuthMessage(baseUrl: string): string {
return `agentmemory: AGENTMEMORY_SECRET is configured for plaintext HTTP to ${baseUrl}. Bearer tokens and memory payloads can be observed on the network; use HTTPS or an SSH tunnel.`;
}
export function createPlaintextBearerAuthGuard(
warn: (message: string) => void = (message) => console.warn(message),
env?: { AGENTMEMORY_REQUIRE_HTTPS?: string },
): (baseUrl: string, secret?: string) => void {
let warned = false;
return (baseUrl, secret) => {
if (!usesPlaintextBearerAuth(baseUrl, secret)) return;
const message = plaintextBearerAuthMessage(baseUrl);
if ((env || process.env).AGENTMEMORY_REQUIRE_HTTPS === "1") throw new Error(message);
if (!warned) {
warned = true;
warn(message);
}
};
}