Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,26 @@ The [**AI Gateway**](https://portkey.wiki/gh-10) is designed for fast, reliable
npx @portkey-ai/gateway
```
> The Gateway is running on `http://localhost:8787/v1`
>
>
> The Gateway Console is running on `http://localhost:8787/public/`

**With Custom Plugins & Middlewares (npm):**
```bash
# Load custom guardrails and middlewares
npx @portkey-ai/gateway -- --plugins-dir=./my-plugins --middlewares-dir=./my-middlewares --port=8787
```

**Available CLI Options:**
```bash
npx @portkey-ai/gateway [options]

Options:
--port Port to run the gateway on (default: 8787)
--headless Run in headless mode (no console UI)
--plugins-dir Directory containing custom plugins
--middlewares-dir Directory containing custom middlewares
```

<sup>
Deployment guides:
&nbsp; <a href="https://portkey.wiki/gh-18"><img height="12" width="12" src="https://cfassets.portkey.ai/logo/dew-color.svg" /> Portkey Cloud (Recommended)</a>
Expand Down Expand Up @@ -227,6 +244,24 @@ Insights from analyzing 2 trillion+ tokens, across 90+ regions and 650+ teams in

<br>

## Extensibility

### Custom Plugins & Middlewares
Extend the AI Gateway with your own custom plugins and middlewares without modifying the core codebase:

- **[Custom Guardrail Plugins](./external-examples/plugins/default-external/)**: Create custom validation and transformation plugins using the same structure as built-in plugins. Load them at runtime with `--plugins-dir` flag.
- **[Custom Middlewares](./external-examples/middlewares/)**: Add custom request/response middleware for authentication, logging, transformation, and more.

**Quick Start:**
```bash
# Load custom plugins and middlewares
npx @portkey-ai/gateway --plugins-dir=./my-plugins --middlewares-dir=./my-middlewares --port=8787
```

See the [Plugin Development Guide](./plugins/README.md) for detailed instructions on creating custom plugins.

<br>

## Cookbooks

### ☄️ Trending
Expand Down
28 changes: 28 additions & 0 deletions external-examples/middlewares/loggerExternal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// External example middleware - simplified version for proof of concept
import { Context } from 'hono';

export const middleware = async (c: Context, next: any) => {
const startTime = Date.now();
const method = c.req.method;
const url = c.req.url;

// Log incoming request
console.log(
`🔷 [External Logger] Incoming request: ${method} ${url.split(':')[1] || url}`
);

// Call next middleware
await next();

// Log outgoing response
const duration = Date.now() - startTime;
console.log(
`🔷 [External Logger] Response sent: ${c.res.status} (${duration}ms)`
);
};

export const metadata = {
name: 'loggerExternal',
description: 'External logger middleware example',
pattern: '*',
};
12 changes: 12 additions & 0 deletions external-examples/middlewares/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "external-example-middlewares",
"version": "1.0.0",
"description": "Example external middlewares for Portkey Gateway",
"type": "module",
"dependencies": {
"dotenv": "^16.0.0"
},
"peerDependencies": {
"portkey-ai": "^1.9.1"
}
}
185 changes: 185 additions & 0 deletions external-examples/middlewares/webhookSignatureVerifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Webhook Signature Verifier Plugin Middleware
*
* Demonstrates how external middleware can register custom routes to a Hono app.
* This example validates HMAC-SHA256 signatures on webhook requests.
*
* Usage:
* npm run start:node -- --middlewares-dir=./external-examples/middlewares --port=8787
*
* Then test with:
* curl -X POST http://localhost:8787/webhooks/verify \
* -H "Content-Type: application/json" \
* -H "X-Signature: <HMAC-SHA256 signature>" \
* -d '{"event":"order.created","data":{"orderId":"123"}}'
*/

import { createHmac, timingSafeEqual } from 'node:crypto';

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'demo-secret-key';

/**
* Compute HMAC-SHA256 signature of the request body
*/
function computeSignature(body, secret) {
const hmac = createHmac('sha256', secret);
hmac.update(body);
return hmac.digest('hex');
}

/**
* Plugin-style middleware factory
* Returns a function that receives the Hono app instance
*/
export const middleware = () => {
return (app) => {
console.log('[WebhookVerifier] Registering route: POST /webhooks/verify');

/**
* Webhook signature verification endpoint
*
* This demonstrates that external middleware can:
* 1. Register custom routes to the app
* 2. Handle request/response like any standard Hono handler
* 3. Access app context and utilities
*
* Request headers:
* - X-Signature: HMAC-SHA256 signature of the body
* - X-Timestamp: (optional) Timestamp for replay protection
*
* Response:
* - 200: Signature valid, webhook processed
* - 401: Missing signature header
* - 403: Invalid signature
* - 400: Invalid request format
*/
app.post('/webhooks/verify', async (c) => {
try {
// 1. Extract signature from headers
const providedSignature = c.req.header('x-signature');

if (!providedSignature) {
return c.json(
{
error: 'Missing signature',
message: 'X-Signature header is required',
},
401
);
}

// 2. Read request body
const body = await c.req.text();

if (!body) {
return c.json(
{
error: 'Empty body',
message: 'Request body cannot be empty',
},
400
);
}

// 3. Compute expected signature
const expectedSignature = computeSignature(body, WEBHOOK_SECRET);

// 4. Verify signature (constant-time comparison to prevent timing attacks)
let isValid = false;
try {
isValid = timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
} catch {
// Length mismatch throws - this is also constant-time via exception
isValid = false;
}

if (!isValid) {
console.log('[WebhookVerifier] Invalid signature detected', {
received: providedSignature.substring(0, 8) + '...',
expected: expectedSignature.substring(0, 8) + '...',
});

return c.json(
{
error: 'Invalid signature',
message: 'Webhook signature verification failed',
},
403
);
}

// 5. Optional: Parse and validate JSON structure
let payload;
try {
payload = JSON.parse(body);
} catch (e) {
return c.json(
{
error: 'Invalid JSON',
message: 'Request body must be valid JSON',
},
400
);
}

// 6. Success: Log and respond
console.log('[WebhookVerifier] Webhook verified', {
event: payload.event,
timestamp: new Date().toISOString(),
dataKeys: Object.keys(payload.data || {}),
});

return c.json(
{
status: 'verified',
message: 'Webhook signature is valid and has been processed',
event: payload.event,
processedAt: new Date().toISOString(),
},
200
);
} catch (error) {
console.error(
'[WebhookVerifier] Error processing webhook:',
error.message
);

return c.json(
{
error: 'Processing error',
message:
error.message || 'An error occurred while processing the webhook',
},
500
);
}
});

// Additional health check endpoint for this webhook verifier
app.get('/webhooks/health', (c) => {
return c.json({
status: 'healthy',
service: 'WebhookSignatureVerifier',
endpoint: 'POST /webhooks/verify',
signingAlgorithm: 'HMAC-SHA256',
});
});
};
};

/**
* Metadata describing this middleware
* Used by Portkey's middleware loader for logging and identification
*/
export const metadata = {
name: 'webhookSignatureVerifier',
description:
'Registers webhook signature verification endpoints (HMAC-SHA256)',
version: '1.0.0',
author: 'Portkey Team',
pattern: '/webhooks/*',
appExtension: true,
};
98 changes: 98 additions & 0 deletions external-examples/plugins/default-external/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"id": "default-external",
"name": "Default External (Proof of Concept)",
"description": "External plugin example - copy of default plugin with renamed IDs",
"credentials": [],
"functions": [
{
"name": "Regex Match External",
"id": "regexMatchExternal",
"_filename": "regexMatchExternal.js",
"type": "guardrail",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"description": [
{
"type": "subHeading",
"text": "Check if the request or response text matches a regex pattern. (EXTERNAL EXAMPLE)"
}
],
"parameters": {
"type": "object",
"properties": {
"rule": {
"type": "string",
"label": "Regex Rule",
"description": [
{
"type": "subHeading",
"text": "Enter the regex pattern"
}
]
},
"not": {
"type": "boolean",
"label": "Invert Match",
"description": [
{
"type": "subHeading",
"text": "If true, the verdict will be inverted"
}
],
"default": false
}
},
"required": ["rule"]
}
},
{
"name": "Word Count External",
"id": "wordCountExternal",
"type": "guardrail",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"description": [
{
"type": "subHeading",
"text": "Checks if the content contains a certain number of words. Ranges allowed. (EXTERNAL EXAMPLE)"
}
],
"parameters": {
"type": "object",
"properties": {
"minWords": {
"type": "number",
"label": "Minimum Word Count",
"description": [
{
"type": "subHeading",
"text": "Enter the minimum number of words to allow."
}
],
"default": 0
},
"maxWords": {
"type": "number",
"label": "Maximum Word Count",
"description": [
{
"type": "subHeading",
"text": "Enter the maximum number of words to allow."
}
],
"default": 99999
},
"not": {
"type": "boolean",
"label": "Invert Range Check",
"description": [
{
"type": "subHeading",
"text": "If true, the verdict will be true when count is outside the range"
}
],
"default": false
}
}
}
}
]
}
Loading