Files
2026-07-13 13:39:12 +08:00

330 lines
9.3 KiB
TypeScript

import { getDbInstance } from "../../src/lib/db/core.ts";
const MAX_SIGNATURES = 1000;
const MAX_PERSISTED_SIGNATURES = 2_000;
const MEMORY_TTL_MS = 1000 * 60 * 60;
const PERSISTED_TTL_MS = 1000 * 60 * 60 * 24 * 30;
const NAMESPACE = "gemini_thought_signatures";
export type SignatureCacheMode = "enabled" | "bypass" | "bypass-strict";
type Entry = {
signature: string;
expiresAt: number;
};
type PersistedEntry = Entry & {
createdAt: number;
};
const signatures = new Map<string, Entry>();
let signatureCacheMode: SignatureCacheMode = "enabled";
let persistedPruneCounter = 0;
const MAX_LOGGED_ERRORS = 50;
const loggedPersistenceErrors = new Set<string>();
function warnPersistenceError(operation: string, error: unknown) {
if (process.env.NODE_ENV === "test") return;
if (loggedPersistenceErrors.has(operation)) return;
if (loggedPersistenceErrors.size >= MAX_LOGGED_ERRORS) {
const first = loggedPersistenceErrors.values().next().value;
if (first !== undefined) loggedPersistenceErrors.delete(first);
}
loggedPersistenceErrors.add(operation);
const message = error instanceof Error ? error.message : String(error);
console.warn(`[signature-cache] persisted ${operation} failed: ${message}`);
}
export function buildGeminiThoughtSignatureKey(namespace: unknown, toolCallId: unknown): unknown {
if (
typeof namespace === "string" &&
namespace.length > 0 &&
typeof toolCallId === "string" &&
toolCallId.length > 0
) {
return `${namespace}:${toolCallId}`;
}
return toolCallId;
}
function pruneExpired() {
const now = Date.now();
for (const [key, value] of signatures.entries()) {
if (value.expiresAt <= now) {
signatures.delete(key);
}
}
while (signatures.size > MAX_SIGNATURES) {
const oldestKey = signatures.keys().next().value;
if (!oldestKey) break;
signatures.delete(oldestKey);
}
}
function serializePersistedEntry(entry: PersistedEntry): string {
return JSON.stringify(entry);
}
function parsePersistedEntry(value: string, now = Date.now()) {
try {
const parsed = JSON.parse(value) as Partial<PersistedEntry>;
if (typeof parsed.signature !== "string" || parsed.signature.length === 0) return null;
const createdAt = typeof parsed.createdAt === "number" ? parsed.createdAt : now;
const expiresAt =
typeof parsed.expiresAt === "number" ? parsed.expiresAt : now + PERSISTED_TTL_MS;
if (expiresAt <= now) return null;
return {
entry: { signature: parsed.signature, createdAt, expiresAt },
shouldRewrite: createdAt !== parsed.createdAt || expiresAt !== parsed.expiresAt,
};
} catch {
if (!value) return null;
return {
entry: {
signature: value,
createdAt: now,
expiresAt: now + PERSISTED_TTL_MS,
},
shouldRewrite: true,
};
}
}
function maybePrunePersistedSignatures(db: ReturnType<typeof getDbInstance>) {
persistedPruneCounter += 1;
if (persistedPruneCounter % 100 !== 0) return;
const rows = db
.prepare("SELECT key, value FROM key_value WHERE namespace = ?")
.all(NAMESPACE) as Array<{ key: string; value: string }>;
const now = Date.now();
const validRows: Array<{ key: string; createdAt: number }> = [];
const keysToDelete = new Set<string>();
for (const row of rows) {
const parsed = parsePersistedEntry(row.value, now);
if (!parsed) {
keysToDelete.add(row.key);
continue;
}
validRows.push({ key: row.key, createdAt: parsed.entry.createdAt });
}
if (rows.length <= MAX_PERSISTED_SIGNATURES && keysToDelete.size === 0) return;
validRows.sort((a, b) => b.createdAt - a.createdAt);
for (const row of validRows.slice(MAX_PERSISTED_SIGNATURES)) {
keysToDelete.add(row.key);
}
if (keysToDelete.size === 0) return;
const remove = db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?");
const tx = db.transaction((keys: string[]) => {
for (const key of keys) remove.run(NAMESPACE, key);
});
tx([...keysToDelete]);
}
export function storeGeminiThoughtSignature(toolCallId: unknown, signature: unknown) {
if (typeof toolCallId !== "string" || !toolCallId) return;
if (typeof signature !== "string" || !signature) return;
const now = Date.now();
pruneExpired();
signatures.set(toolCallId, {
signature,
expiresAt: now + MEMORY_TTL_MS,
});
try {
const db = getDbInstance();
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
NAMESPACE,
toolCallId,
serializePersistedEntry({ signature, createdAt: now, expiresAt: now + PERSISTED_TTL_MS })
);
maybePrunePersistedSignatures(db);
} catch (error) {
warnPersistenceError("store", error);
}
}
export function getGeminiThoughtSignature(toolCallId: unknown) {
if (typeof toolCallId !== "string" || !toolCallId) return null;
pruneExpired();
const entry = signatures.get(toolCallId);
if (entry) return entry.signature;
try {
const db = getDbInstance();
const row = db
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
.get(NAMESPACE, toolCallId) as { value: string } | undefined;
if (row?.value) {
const persisted = parsePersistedEntry(row.value);
if (!persisted) {
db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(
NAMESPACE,
toolCallId
);
return null;
}
signatures.set(toolCallId, {
signature: persisted.entry.signature,
expiresAt: Date.now() + MEMORY_TTL_MS,
});
if (persisted.shouldRewrite) {
db.prepare("UPDATE key_value SET value = ? WHERE namespace = ? AND key = ?").run(
serializePersistedEntry(persisted.entry),
NAMESPACE,
toolCallId
);
}
return persisted.entry.signature;
}
} catch (error) {
warnPersistenceError("read", error);
}
return null;
}
export function normalizeSignatureCacheMode(value: unknown): SignatureCacheMode {
return value === "bypass" || value === "bypass-strict" ? value : "enabled";
}
export function setGeminiThoughtSignatureMode(mode: unknown) {
signatureCacheMode = normalizeSignatureCacheMode(mode);
}
export function getGeminiThoughtSignatureMode(): SignatureCacheMode {
return signatureCacheMode;
}
function decodeSignature(signature: string): Buffer | null {
if (!signature || (signature[0] !== "R" && signature[0] !== "E")) return null;
const payload = signature.slice(1);
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(payload) || payload.length % 4 === 1) {
return null;
}
try {
const decoded = Buffer.from(payload, "base64");
if (decoded.length === 0) return null;
const canonical = decoded.toString("base64").replace(/=+$/g, "");
if (canonical !== payload.replace(/=+$/g, "")) {
return null;
}
return decoded;
} catch {
return null;
}
}
function readVarint(
buffer: Buffer,
startOffset: number
): { nextOffset: number; value: number } | null {
let offset = startOffset;
let result = 0;
let shift = 0;
while (offset < buffer.length && shift < 35) {
const byte = buffer[offset];
result |= (byte & 0x7f) << shift;
offset += 1;
if ((byte & 0x80) === 0) {
return { nextOffset: offset, value: result };
}
shift += 7;
}
return null;
}
export function isValidBasicGeminiThoughtSignature(signature: unknown): boolean {
if (typeof signature !== "string") return false;
const decoded = decodeSignature(signature);
return Boolean(decoded && decoded[0] === 0x12);
}
export function isValidFullGeminiThoughtSignature(signature: unknown): boolean {
if (typeof signature !== "string") return false;
const decoded = decodeSignature(signature);
if (!decoded || decoded[0] !== 0x12) return false;
const outerLength = readVarint(decoded, 1);
if (!outerLength) return false;
const outerEnd = outerLength.nextOffset + outerLength.value;
if (outerEnd !== decoded.length) return false;
const inner = decoded.subarray(outerLength.nextOffset, outerEnd);
if (inner.length === 0 || inner[0] !== 0x0a) return false;
const innerLength = readVarint(inner, 1);
if (!innerLength) return false;
return innerLength.nextOffset + innerLength.value === inner.length;
}
export function resolveGeminiThoughtSignature(
toolCallId: unknown,
clientSignature?: unknown
): string | null {
const persisted = getGeminiThoughtSignature(toolCallId);
if (typeof clientSignature !== "string" || clientSignature.length === 0) {
return persisted;
}
if (signatureCacheMode === "enabled") {
return persisted;
}
const isValid =
signatureCacheMode === "bypass-strict"
? isValidFullGeminiThoughtSignature(clientSignature)
: isValidBasicGeminiThoughtSignature(clientSignature);
if (isValid) {
return clientSignature;
}
console.warn(
`[signature-cache] ${signatureCacheMode}: invalid client thought signature, falling back`
);
return persisted;
}
export function clearGeminiThoughtSignatures() {
signatures.clear();
signatureCacheMode = "enabled";
try {
const db = getDbInstance();
db.prepare("DELETE FROM key_value WHERE namespace = ?").run(NAMESPACE);
} catch (error) {
warnPersistenceError("clear", error);
}
}
export function clearGeminiThoughtSignatureMemoryForTests() {
signatures.clear();
}
export function getGeminiThoughtSignatureMemorySizeForTests() {
pruneExpired();
return signatures.size;
}