Enable LLMs to handle webhooks with plaintext files
at main 84 lines 2.7 kB view raw
1import * as nodePath from "node:path"; 2import * as fs from "node:fs/promises"; 3import matter from "gray-matter"; 4import { validateFrontmatter, LureParseError } from "./schema.js"; 5import type { ParsedLure } from "./types.js"; 6import type { StandardSchemaV1 } from "@standard-schema/spec"; 7 8export { LureParseError }; 9 10export class LureCache { 11 private readonly byLurePath = new Map<string, ParsedLure>(); 12 private readonly byFilePath = new Map<string, string>(); 13 14 constructor( 15 private readonly luresDir: string, 16 private readonly allowUnverified: boolean, 17 private readonly configSchema: StandardSchemaV1 | undefined, 18 ) {} 19 20 async load(): Promise<void> { 21 const entries = await fs.readdir(this.luresDir, { recursive: true }); 22 for (const entry of entries) { 23 if (typeof entry === "string" && entry.endsWith(".lure")) { 24 const filePath = nodePath.join(this.luresDir, entry); 25 try { 26 await this.set(filePath); 27 } catch (error) { 28 console.error(`Failed to load lure ${filePath}:`, error); 29 } 30 } 31 } 32 } 33 34 async set(filePath: string): Promise<void> { 35 const resolved = nodePath.resolve(filePath); 36 const resolvedDir = nodePath.resolve(this.luresDir); 37 38 if (!resolved.startsWith(resolvedDir + nodePath.sep) && resolved !== resolvedDir) { 39 throw new LureParseError(`Path traversal detected: ${filePath}`, filePath); 40 } 41 42 const content = await fs.readFile(filePath, "utf-8"); 43 const { data, content: template } = matter(content); 44 45 const frontmatter = await validateFrontmatter(data, filePath, this.configSchema); 46 47 if (!this.allowUnverified && frontmatter.verify === undefined) { 48 throw new LureParseError( 49 `Lure has no verify block but allowUnverified is false: ${filePath}`, 50 filePath, 51 ); 52 } 53 54 const relative = nodePath.relative(resolvedDir, nodePath.resolve(filePath)); 55 const lurePath = "/" + relative.replace(/\.lure$/, "").replace(/\\/g, "/"); 56 57 const parsed: ParsedLure = { 58 filePath, 59 lurePath, 60 frontmatter, 61 template: template.trim(), 62 }; 63 64 const oldLurePath = this.byFilePath.get(filePath); 65 if (oldLurePath !== undefined) { 66 this.byLurePath.delete(oldLurePath); 67 } 68 69 this.byLurePath.set(lurePath, parsed); 70 this.byFilePath.set(filePath, lurePath); 71 } 72 73 delete(filePath: string): void { 74 const lurePath = this.byFilePath.get(filePath); 75 if (lurePath !== undefined) { 76 this.byLurePath.delete(lurePath); 77 this.byFilePath.delete(filePath); 78 } 79 } 80 81 get(lurePath: string): ParsedLure | undefined { 82 return this.byLurePath.get(lurePath); 83 } 84}