# LuaWebhook

The `LuaWebhook` class defines HTTP endpoints that can receive external requests with validated query parameters, headers, and body. Use webhooks to integrate with external services and trigger agent actions.

## Import

```typescript
import { LuaWebhook } from 'lua-cli';
import { z } from 'zod';
```

## Overview

Webhooks enable:
- Receive HTTP requests from external services
- Validate query parameters, headers, and body
- Process events from third-party platforms
- Trigger agent actions based on external events
- Build integrations with APIs and services

## Class Definition

```typescript
class LuaWebhook {
  constructor(config: LuaWebhookConfig);
  getName(): string;
  getDescription(): string;
  execute(query?, headers?, body?): Promise<any>;
}
```

## Configuration

```typescript
interface LuaWebhookConfig {
  /** Webhook name (optional, defaults to 'unnamed-webhook') */
  name?: string;

  /** Short description of the webhook (1-2 sentences) */
  description: string;

  /** Optional Zod schema for query parameter validation */
  querySchema?: ZodType;

  /** Optional Zod schema for header validation */
  headerSchema?: ZodType;

  /** Optional Zod schema for body validation */
  bodySchema?: ZodType;

  /** Function that executes the webhook logic */
  execute: (event: LuaWebhookEvent) => Promise<any>;
}
```

## Event Structure

The `execute` function receives a `LuaWebhookEvent`:

```typescript
interface LuaWebhookEvent {
  /** Parsed query parameters */
  query?: Record<string, any>;

  /** Request headers */
  headers?: Record<string, any>;

  /** Request body (JSON or raw data) */
  body?: any;

  /** ISO timestamp when the webhook was received */
  timestamp: string;
}
```

## Complete Examples

### GitHub Webhook

```typescript
import { LuaWebhook, User, Data } from 'lua-cli';
import { z } from 'zod';

export const githubWebhook = new LuaWebhook({
  name: 'github-push',
  description: 'Handles GitHub push events',

  headerSchema: z.object({
    'x-github-event': z.string(),
    'x-hub-signature': z.string()
  }),

  bodySchema: z.object({
    repository: z.object({
      name: z.string(),
      full_name: z.string()
    }),
    pusher: z.object({
      name: z.string(),
      email: z.string()
    }),
    commits: z.array(z.object({
      message: z.string(),
      url: z.string()
    }))
  }),

  execute: async (event) => {
    const { headers, body } = event;

    // Verify it's a push event
    if (headers['x-github-event'] !== 'push') {
      return {
        success: false,
        message: 'Only push events are supported'
      };
    }

    // Store commit information
    for (const commit of body.commits) {
      await Data.create('commits', {
        repository: body.repository.full_name,
        author: body.pusher.name,
        message: commit.message,
        url: commit.url,
        timestamp: event.timestamp
      });
    }

    // Notify relevant user
    const user = await User.get({ email: body.pusher.email });
    if (user) {
      await user.send([{
        type: 'text',
        text: `✅ ${body.commits.length} commits pushed to ${body.repository.name}`
      }]);
    }

    return {
      success: true,
      commits: body.commits.length,
      repository: body.repository.name
    };
  }
});
```

### Stripe Payment Webhook

```typescript
import { LuaWebhook, User, Orders, OrderStatus } from 'lua-cli';
import { z } from 'zod';

export const stripeWebhook = new LuaWebhook({
  name: 'stripe-payment',
  description: 'Handles Stripe payment events',

  headerSchema: z.object({
    'stripe-signature': z.string()
  }),

  bodySchema: z.object({
    type: z.string(),
    data: z.object({
      object: z.object({
        id: z.string(),
        amount: z.number(),
        currency: z.string(),
        customer: z.string().optional(),
        metadata: z.record(z.any()).optional()
      })
    })
  }),

  execute: async (event) => {
    const { body } = event;

    // Handle different event types
    switch (body.type) {
      case 'payment_intent.succeeded': {
        const payment = body.data.object;
        const orderId = payment.metadata?.orderId;

        if (orderId) {
          // Update order status
          const order = await Orders.getById(orderId);
          await order.updateStatus(OrderStatus.PROCESSING);
          await order.update({
            paymentId: payment.id,
            paidAmount: payment.amount / 100,
            paidAt: event.timestamp
          });

          // Notify customer
          if (payment.customer) {
            const user = await User.get(payment.customer);
            await user.send([{
              type: 'text',
              text: `✅ Payment received! Your order #${orderId} is being processed.`
            }]);
          }
        }

        return {
          success: true,
          event: 'payment_succeeded',
          orderId
        };
      }

      case 'payment_intent.payment_failed': {
        const payment = body.data.object;
        const orderId = payment.metadata?.orderId;

        // Notify customer of failure
        if (payment.customer) {
          const user = await User.get(payment.customer);
          await user.send([{
            type: 'text',
            text: `❌ Payment failed for order #${orderId}. Please try again.`
          }]);
        }

        return {
          success: true,
          event: 'payment_failed',
          orderId
        };
      }

      default:
        return {
          success: true,
          event: body.type,
          message: 'Event received but not handled'
        };
    }
  }
});
```

### Slack Message Webhook

```typescript
import { LuaWebhook, Data, AI } from 'lua-cli';
import { z } from 'zod';

export const slackWebhook = new LuaWebhook({
  name: 'slack-message',
  description: 'Receives messages from Slack',

  bodySchema: z.object({
    type: z.string(),
    challenge: z.string().optional(),
    event: z.object({
      type: z.string(),
      user: z.string().optional(),
      text: z.string().optional(),
      channel: z.string()
    }).optional()
  }),

  execute: async (event) => {
    const { body } = event;

    // Handle Slack challenge (verification)
    if (body.type === 'url_verification') {
      return { challenge: body.challenge };
    }

    // Handle message events
    if (body.type === 'event_callback' && body.event) {
      const slackEvent = body.event;

      // Ignore bot messages
      if (slackEvent.user === 'USLACKBOT') {
        return { success: true, message: 'Ignored bot message' };
      }

      // Process user message
      if (slackEvent.type === 'message' && slackEvent.text) {
        // Generate AI response
        const response = await AI.generate(
          'You are a helpful Slack bot assistant.',
          [{ type: 'text', text: slackEvent.text }]
        );

        // Store interaction
        await Data.create('slack_messages', {
          user: slackEvent.user,
          channel: slackEvent.channel,
          message: slackEvent.text,
          response,
          timestamp: event.timestamp
        });

        // TODO: Send response back to Slack using Slack API

        return {
          success: true,
          response
        };
      }
    }

    return { success: true, message: 'Event received' };
  }
});
```

### Form Submission Webhook

```typescript
import { LuaWebhook, User, Data } from 'lua-cli';
import { z } from 'zod';

export const contactFormWebhook = new LuaWebhook({
  name: 'contact-form',
  description: 'Handles contact form submissions',

  querySchema: z.object({
    source: z.enum(['website', 'mobile_app']).optional()
  }),

  bodySchema: z.object({
    name: z.string().min(2),
    email: z.string().email(),
    phone: z.string().optional(),
    subject: z.string(),
    message: z.string().min(10),
    consent: z.boolean()
  }),

  execute: async (event) => {
    const { query, body } = event;

    // Verify consent
    if (!body.consent) {
      return {
        success: false,
        error: 'Consent is required'
      };
    }

    // Store submission
    const entry = await Data.create('contact_submissions', {
      name: body.name,
      email: body.email,
      phone: body.phone,
      subject: body.subject,
      message: body.message,
      source: query?.source || 'unknown',
      submittedAt: event.timestamp,
      status: 'new'
    });

    // Create or update user
    let user = await User.get({ email: body.email });
    if (!user) {
      user = await User.get(); // Create new user context
      await user.update({
        name: body.name,
        email: body.email,
        phone: body.phone
      });
    }

    // Send confirmation
    await user.send([{
      type: 'text',
      text: `Thank you for contacting us, ${body.name}! We've received your message about "${body.subject}" and will get back to you soon.`
    }]);

    return {
      success: true,
      submissionId: entry.id,
      message: 'Form submitted successfully'
    };
  }
});
```

### API Integration Webhook

```typescript
import { LuaWebhook, Data } from 'lua-cli';
import { z } from 'zod';

export const apiWebhook = new LuaWebhook({
  name: 'external-api',
  description: 'Receives data from external API',

  headerSchema: z.object({
    'x-api-key': z.string(),
    'content-type': z.literal('application/json')
  }),

  querySchema: z.object({
    action: z.enum(['create', 'update', 'delete']),
    entity: z.string()
  }),

  bodySchema: z.object({
    id: z.string(),
    data: z.record(z.any())
  }),

  execute: async (event) => {
    const { query, headers, body } = event;

    // Verify API key
    const validApiKey = process.env.WEBHOOK_API_KEY;
    if (headers['x-api-key'] !== validApiKey) {
      return {
        success: false,
        error: 'Invalid API key'
      };
    }

    // Handle different actions
    switch (query.action) {
      case 'create':
        await Data.create(query.entity, {
          externalId: body.id,
          ...body.data,
          syncedAt: event.timestamp
        });
        break;

      case 'update':
        const entries = await Data.search(query.entity, body.id);
        if (entries.data.length > 0) {
          await Data.update(query.entity, entries.data[0].id, {
            ...body.data,
            syncedAt: event.timestamp
          });
        }
        break;

      case 'delete':
        const toDelete = await Data.search(query.entity, body.id);
        if (toDelete.data.length > 0) {
          await Data.delete(query.entity, toDelete.data[0].id);
        }
        break;
    }

    return {
      success: true,
      action: query.action,
      entity: query.entity,
      id: body.id
    };
  }
});
```

## Webhook URL

After deploying your webhook, it will be available at:

```
https://api.heylua.ai/webhook/{agentId}/{webhookName}
```

Example:
```
https://api.heylua.ai/webhook/agent_123/github-push
```

## Testing Webhooks

### Local Testing with lua dev

```bash
# Start dev mode
lua dev

# Webhook will be available at:
# http://localhost:3002/webhook/{webhookName}
```

### Testing with curl

```bash
# Test webhook locally
curl -X POST http://localhost:3002/webhook/contact-form \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "subject": "Question",
    "message": "This is a test message",
    "consent": true
  }'
```

## Best Practices

### ✅ DO

- Always validate input with schemas (query, headers, body)
- Verify webhook signatures when available (GitHub, Stripe, etc.)
- Return meaningful response objects
- Store webhook events for auditing
- Handle errors gracefully
- Use appropriate HTTP status codes in responses
- Implement idempotency for critical operations
- Log webhook events for debugging

### ❌ DON'T

- Don't skip validation - always use schemas
- Don't expose sensitive data in responses
- Don't perform long-running operations synchronously
- Don't trust incoming data without verification
- Don't forget to handle webhook retry logic
- Don't ignore timestamp for event ordering

## Error Handling

```typescript
export const webhook = new LuaWebhook({
  name: 'robust-webhook',
  description: 'Webhook with error handling',

  bodySchema: z.object({
    data: z.string()
  }),

  execute: async (event) => {
    try {
      // Process webhook
      const result = await processData(event.body.data);

      return {
        success: true,
        result
      };
    } catch (error) {
      // Log error
      await Data.create('webhook_errors', {
        webhookName: 'robust-webhook',
        error: error.message,
        event: event.body,
        timestamp: event.timestamp
      });

      // Return error response
      return {
        success: false,
        error: error.message,
        retry: true // Indicate that the sender should retry
      };
    }
  }
});
```

## Security

### Verify Signatures

```typescript
import crypto from 'crypto';

function verifyGitHubSignature(
  body: string,
  signature: string,
  secret: string
): boolean {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = 'sha256=' + hmac.update(body).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}
```

### API Key Validation

```typescript
headerSchema: z.object({
  'x-api-key': z.string()
    .refine(key => key === process.env.WEBHOOK_API_KEY, {
      message: 'Invalid API key'
    })
})
```

## Related APIs

- [LuaJob](./LuaJob.md) - Schedule recurring tasks
- [Data API](./Data.md) - Store webhook events
- [User API](./User.md) - Notify users about events

## See Also

- [Webhook Integration Guide](../guides/Webhooks.md)
- [Third-Party Integrations](../guides/Integrations.md)
