{"version":3,"file":"webhook-routes.mjs","names":[],"sources":["../../../src/services/webhook-routes.ts"],"sourcesContent":["/**\n * Webhook Routes — registry that maps URL paths to event triggers.\n *\n * Each route defines:\n * - A URL path (e.g. \"/github\", \"/stripe\", \"/monitor\")\n * - A source label for display\n * - An optional HMAC secret for signature verification\n * - An optional plan name to trigger when the webhook fires\n * - Enable/disable lifecycle\n *\n * Routes persist to ~/.openclawnch/webhooks/routes.json.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport interface WebhookRoute {\n  /** Unique ID. */\n  id: string;\n  /** Route name (alphanumeric + hyphens, unique). */\n  name: string;\n  /** URL path this route matches (e.g. \"/github\"). */\n  path: string;\n  /** Human-readable source label (e.g. \"GitHub\", \"Stripe Payments\"). */\n  source: string;\n  /** HMAC secret for signature verification. Empty = no verification (not recommended). */\n  secret: string;\n  /** Plan name to trigger when webhook fires. Empty = just emit event. */\n  triggerPlan: string;\n  /** Whether this route is active. */\n  enabled: boolean;\n  /** Who created this route. */\n  createdBy: string;\n  /** Number of times this webhook has been received. */\n  hitCount: number;\n  createdAt: number;\n  updatedAt: number;\n}\n\n// ─── Validation ─────────────────────────────────────────────────────────\n\nfunction isValidRouteName(name: string): boolean {\n  return /^[a-z][a-z0-9\\-]{1,40}$/.test(name);\n}\n\nfunction isValidPath(path: string): boolean {\n  return /^\\/[a-z0-9\\-_\\/]{0,100}$/.test(path);\n}\n\n// ─── Service ────────────────────────────────────────────────────────────\n\nexport class WebhookRouteRegistry {\n  private routes = new Map<string, WebhookRoute>();\n  private stateDir: string;\n\n  constructor(opts?: { stateDir?: string }) {\n    this.stateDir = opts?.stateDir ?? join(\n      process.env.HOME ?? '', '.openclawnch', 'webhooks'\n    );\n    this.loadState();\n  }\n\n  /** Create a new webhook route. */\n  create(params: {\n    name: string;\n    path: string;\n    source: string;\n    secret?: string;\n    triggerPlan?: string;\n    createdBy: string;\n  }): WebhookRoute {\n    if (!isValidRouteName(params.name)) {\n      throw new WebhookRouteError(\n        `Invalid route name \"${params.name}\". Must be 2-40 chars, lowercase alphanumeric + hyphens.`\n      );\n    }\n    if (!isValidPath(params.path)) {\n      throw new WebhookRouteError(\n        `Invalid path \"${params.path}\". Must start with / and contain only lowercase alphanumeric, hyphens, underscores.`\n      );\n    }\n    if (this.getByName(params.name)) {\n      throw new WebhookRouteError(`A route named \"${params.name}\" already exists.`);\n    }\n    if (this.getByPath(params.path)) {\n      throw new WebhookRouteError(`A route with path \"${params.path}\" already exists.`);\n    }\n\n    const id = `wh_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n    const now = Date.now();\n\n    const route: WebhookRoute = {\n      id,\n      name: params.name,\n      path: params.path,\n      source: params.source,\n      secret: params.secret ?? '',\n      triggerPlan: params.triggerPlan ?? '',\n      enabled: true,\n      createdBy: params.createdBy,\n      hitCount: 0,\n      createdAt: now,\n      updatedAt: now,\n    };\n\n    this.routes.set(id, route);\n    this.saveState();\n    return route;\n  }\n\n  update(id: string, updates: Partial<Pick<WebhookRoute,\n    'source' | 'secret' | 'triggerPlan' | 'enabled'\n  >>): WebhookRoute | null {\n    const route = this.routes.get(id);\n    if (!route) return null;\n    Object.assign(route, updates, { updatedAt: Date.now() });\n    this.saveState();\n    return route;\n  }\n\n  delete(id: string): boolean {\n    const existed = this.routes.delete(id);\n    if (existed) this.saveState();\n    return existed;\n  }\n\n  get(id: string): WebhookRoute | null {\n    return this.routes.get(id) ?? null;\n  }\n\n  getByName(name: string): WebhookRoute | null {\n    for (const r of this.routes.values()) {\n      if (r.name === name) return r;\n    }\n    return null;\n  }\n\n  getByPath(path: string): WebhookRoute | null {\n    for (const r of this.routes.values()) {\n      if (r.path === path) return r;\n    }\n    return null;\n  }\n\n  list(opts?: { enabled?: boolean }): WebhookRoute[] {\n    let all = Array.from(this.routes.values());\n    if (opts?.enabled !== undefined) all = all.filter(r => r.enabled === opts.enabled);\n    return all.sort((a, b) => b.updatedAt - a.updatedAt);\n  }\n\n  recordHit(id: string): void {\n    const route = this.routes.get(id);\n    if (route) {\n      route.hitCount += 1;\n      route.updatedAt = Date.now();\n      this.saveState();\n    }\n  }\n\n  clear(): void {\n    this.routes.clear();\n  }\n\n  // ── Persistence ─────────────────────────────────────────────────────\n\n  private loadState(): void {\n    try {\n      const filePath = join(this.stateDir, 'routes.json');\n      if (existsSync(filePath)) {\n        const data = JSON.parse(readFileSync(filePath, 'utf8'));\n        for (const r of data) {\n          this.routes.set(r.id, r);\n        }\n      }\n    } catch { /* fresh start */ }\n  }\n\n  private saveState(): void {\n    try {\n      if (!existsSync(this.stateDir)) mkdirSync(this.stateDir, { recursive: true });\n      const filePath = join(this.stateDir, 'routes.json');\n      writeFileSync(filePath, JSON.stringify(Array.from(this.routes.values()), null, 2), 'utf8');\n    } catch { /* best effort */ }\n  }\n}\n\n// ─── Error Class ────────────────────────────────────────────────────────\n\nexport class WebhookRouteError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'WebhookRouteError';\n  }\n}\n\n// ─── Singleton ──────────────────────────────────────────────────────────\n\nlet instance: WebhookRouteRegistry | null = null;\n\nexport function getWebhookRoutes(opts?: { stateDir?: string }): WebhookRouteRegistry {\n  if (!instance) {\n    instance = new WebhookRouteRegistry(opts);\n  }\n  return instance;\n}\n\nexport function resetWebhookRoutes(): void {\n  instance?.clear();\n  instance = null;\n}\n"],"mappings":"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"}