Rename directories
This commit is contained in:
53
examples/deployment/pipecat-cloud-daily-pstn-server/nextjs-webhook-server/.gitignore
vendored
Normal file
53
examples/deployment/pipecat-cloud-daily-pstn-server/nextjs-webhook-server/.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,105 @@
|
||||
# Next.js server for handling Daily PSTN/SIP Webhook
|
||||
|
||||
Next.js API routes for handling Daily PSTN/SIP Pipecat requests.
|
||||
|
||||
## Features
|
||||
|
||||
- API endpoint for handling Daily PSTN/SIP Pipecat requests
|
||||
- HMAC signature validation
|
||||
- Structured logging with Pino
|
||||
- Support for dial-in and dial-out settings
|
||||
- Voicemail detection and call transfer functionality
|
||||
- Test request handling
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
2. Navigate to the `nextjs-webhook-server` directory:
|
||||
|
||||
```bash
|
||||
cd nextjs-webhook-server
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Create `.env.local` file with your credentials:
|
||||
|
||||
```bash
|
||||
cp env.local.example .env.local
|
||||
```
|
||||
|
||||
5. Update your `.env` with your secrets:
|
||||
|
||||
```bash
|
||||
PIPECAT_CLOUD_API_KEY=pk_*
|
||||
AGENT_NAME=my-first-agent
|
||||
PINLESS_HMAC_SECRET=your_hmac_secret
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
6. Run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /api
|
||||
|
||||
Returns a simple "Hello, World!" message with a cute cat emoji to verify the server is running.
|
||||
|
||||
### POST /api/dial
|
||||
|
||||
Handles dial-in and dial-out requests for Pipecat Cloud.
|
||||
|
||||
#### Test Requests
|
||||
|
||||
The endpoint handles test requests when a webhook is configured. Send a request with `"Test": "test"` to verify your setup:
|
||||
|
||||
```json
|
||||
{
|
||||
"Test": "test"
|
||||
}
|
||||
```
|
||||
|
||||
#### Production Request Format
|
||||
|
||||
```json
|
||||
{
|
||||
// for dial-in from webhook
|
||||
"To": "+14152251493",
|
||||
"From": "+14158483432",
|
||||
"callId": "string-contains-uuid",
|
||||
"callDomain": "string-contains-uuid",
|
||||
// for making a dial out to a phone or SIP
|
||||
"dialout_settings": [
|
||||
{ "phoneNumber": "+14158483432", "callerId": "purchased_phone_uuid" },
|
||||
{ "sipUri": "sip:username@sip.hostname.com" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
The application is configured for Vercel deployment:
|
||||
|
||||
1. Push your code to a Git repository
|
||||
2. Import your project in Vercel dashboard
|
||||
3. Configure environment variables:
|
||||
- `PIPECAT_CLOUD_API_KEY`
|
||||
- `AGENT_NAME`
|
||||
- `PINLESS_HMAC_SECRET`
|
||||
- `LOG_LEVEL` (optional, defaults to 'info')
|
||||
4. Deploy!
|
||||
|
||||
## Security
|
||||
|
||||
- HMAC signature validation for request authentication
|
||||
- Environment variables for sensitive credentials
|
||||
- Method validation (POST only for /dial)
|
||||
@@ -0,0 +1,4 @@
|
||||
PINLESS_HMAC_SECRET=your_hmac_secret
|
||||
PIPECAT_CLOUD_API_KEY=your_daily_api_key
|
||||
AGENT_NAME=my-first-agent
|
||||
PORT=8000
|
||||
5447
examples/deployment/pipecat-cloud-daily-pstn-server/nextjs-webhook-server/package-lock.json
generated
Normal file
5447
examples/deployment/pipecat-cloud-daily-pstn-server/nextjs-webhook-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "my-daily-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 7860",
|
||||
"build": "next build",
|
||||
"start": "next start -p 7860",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"next": "^14.0.0",
|
||||
"pino": "^8.15.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-next": "^14.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { logger } from '../../lib/utils';
|
||||
import axios from 'axios';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const validateSignature = (body, signature, timestamp, secret) => {
|
||||
// Skip if any required fields are missing
|
||||
if (!signature || !timestamp || !secret) {
|
||||
logger.warn('Missing required fields for HMAC validation');
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedSecret = Buffer.from(secret, 'base64');
|
||||
const hmac = crypto.createHmac('sha256', decodedSecret);
|
||||
const signatureData = `${timestamp}.${body}`;
|
||||
const computedSignature = hmac.update(signatureData).digest('base64');
|
||||
|
||||
logger.debug('Signature validation:', {
|
||||
timestamp,
|
||||
signatureData: signatureData.substring(0, 50) + '...',
|
||||
computedSignature,
|
||||
receivedSignature: signature
|
||||
});
|
||||
|
||||
return computedSignature === signature;
|
||||
} catch (error) {
|
||||
logger.error('Error validating signature:', error);
|
||||
return true; // Allow request to proceed on error
|
||||
}
|
||||
};
|
||||
|
||||
export default async function handler(req, res) {
|
||||
// Only allow POST requests
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Incoming request to /api/dial:');
|
||||
logger.info(`Headers: ${JSON.stringify(req.headers)}`);
|
||||
|
||||
const rawBody = JSON.stringify(req.body);
|
||||
logger.info(`Raw body: ${rawBody}`);
|
||||
|
||||
const signature = req.headers['x-pinless-signature'];
|
||||
const timestamp = req.headers['x-pinless-timestamp'];
|
||||
|
||||
if (signature && timestamp) {
|
||||
logger.info('Validating HMAC signature');
|
||||
if (!validateSignature(rawBody, signature, timestamp, process.env.PINLESS_HMAC_SECRET)) {
|
||||
logger.error('Invalid HMAC signature', { signature, timestamp });
|
||||
return res.status(401).json({
|
||||
error: 'Invalid signature',
|
||||
message: 'Invalid HMAC signature'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.info('Skipping HMAC validation - no signature headers present');
|
||||
}
|
||||
|
||||
// Extract request data
|
||||
const {
|
||||
Test: test,
|
||||
To,
|
||||
From,
|
||||
callId,
|
||||
callDomain,
|
||||
dialout_settings,
|
||||
voicemail_detection,
|
||||
call_transfer
|
||||
} = req.body;
|
||||
|
||||
// Handle test requests when a webhook is configured
|
||||
if (test === 'test') {
|
||||
logger.debug('Test request received');
|
||||
return res.status(200).json({ status: 'success', message: 'Test request received' });
|
||||
}
|
||||
|
||||
// Process dialin settings
|
||||
let dialin_settings = null;
|
||||
const requiredFields = ['To', 'From', 'callId', 'callDomain'];
|
||||
|
||||
if (requiredFields.every(field => req.body[field] !== undefined && req.body[field] !== null)) {
|
||||
dialin_settings = {
|
||||
// snake_case because pipecat expects this format
|
||||
From,
|
||||
To,
|
||||
call_id: callId,
|
||||
call_domain: callDomain,
|
||||
};
|
||||
logger.debug(`Populated dialin_settings from request: ${JSON.stringify(dialin_settings)}`);
|
||||
}
|
||||
|
||||
// Set up Daily room properties
|
||||
const daily_room_properties = {
|
||||
enable_dialout: dialout_settings !== undefined && dialout_settings !== null,
|
||||
exp: Math.floor(Date.now() / 1000) + (5 * 60), // 5 minutes from now
|
||||
};
|
||||
|
||||
// Configure SIP if dialin settings are provided
|
||||
if (dialin_settings !== null) {
|
||||
const sip_config = {
|
||||
display_name: From,
|
||||
sip_mode: 'dial-in',
|
||||
num_endpoints: call_transfer !== null ? 2 : 1,
|
||||
};
|
||||
daily_room_properties.sip = sip_config;
|
||||
}
|
||||
|
||||
// Prepare payload for {service}/start API call
|
||||
const payload = {
|
||||
createDailyRoom: true,
|
||||
dailyRoomProperties: daily_room_properties,
|
||||
body: {
|
||||
dialin_settings,
|
||||
dialout_settings,
|
||||
voicemail_detection,
|
||||
call_transfer,
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug(`Daily room properties: ${JSON.stringify(daily_room_properties)}`);
|
||||
|
||||
// Get Daily API key and agent name from environment variables
|
||||
const pccApiKey = process.env.PIPECAT_CLOUD_API_KEY;
|
||||
const agentName = process.env.AGENT_NAME || 'my-first-agent';
|
||||
|
||||
if (!pccApiKey) {
|
||||
throw new Error('PIPECAT_CLOUD_API_KEY environment variable is not set');
|
||||
}
|
||||
|
||||
// Set up headers for Daily API call
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${pccApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const url = `https://api.pipecat.daily.co/v1/public/${agentName}/start`;
|
||||
logger.debug(`Making API call to Daily: ${url} ${JSON.stringify(headers)} ${JSON.stringify(payload)}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, payload, { headers });
|
||||
logger.debug(`Response: ${JSON.stringify(response.data)}`);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 'success',
|
||||
data: response.data,
|
||||
room_properties: daily_room_properties,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
// Pass through status code and error details from the Daily API
|
||||
const statusCode = error.response.status;
|
||||
const errorDetail = error.response.data || error.message;
|
||||
logger.error(`HTTP error: ${JSON.stringify(errorDetail)}`);
|
||||
return res.status(statusCode).json(errorDetail);
|
||||
} else {
|
||||
logger.error(`Request error: ${error.message}`);
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Unexpected error: ${error.message}`);
|
||||
return res.status(500).json({ error: 'Internal server error', message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Configure body parser to preserve raw body text
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '1mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { logger } from '../../lib/utils';
|
||||
|
||||
export default function handler(req, res) {
|
||||
logger.info('Received request to /api');
|
||||
res.status(200).json({ message: 'Hello, World! from ᓚᘏᗢ' });
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
version: 2,
|
||||
buildCommand: "next build",
|
||||
outputDirectory: ".next",
|
||||
cleanUrls: true
|
||||
};
|
||||
Reference in New Issue
Block a user