@peacockindia/dify-hono-proxy
v0.1.0
Published
Reusable Hono.js proxy server for Dify Chat Widget - keeps API keys server-side
Maintainers
Readme
@peacock/dify-hono-proxy
Reusable Hono.js proxy server for the Dify Chat Widget. Keeps your Dify API keys server-side — the client only sends a public slug.
Architecture
Client Hono Server / Cloudflare Worker Dify Server
│ │ │
│ public slug + query │ │
│ ──────────────────────────> │ │
│ │ private API key │
│ │ ─────────────────────────────────> │
│ │ │
│ │ streaming response │
│ <────────────────────────── │ <───────────────────────────────── │- Slug is public and safe to expose in client code
- API key is server-only and never sent to the client
- Origins are explicitly approved per-slug or globally
Installation
npm install @peacock/dify-hono-proxy honoQuick Start
import { createDifyProxy } from "@peacock/dify-hono-proxy";
const app = createDifyProxy({
resolveApp: async (slug) => {
const apps = {
"la-antara": {
apiKey: "app-xxxxxxxxxxxx",
baseUrl: "https://api.dify.ai/v1",
publicConfig: {
displayName: "La Antaraa Assistant",
welcomeMessage: "How can I help you?",
logoUrl: "",
primaryColor: "#008F83",
attachmentsEnabled: true,
maxFiles: 3,
maxFileSize: 10,
allowedFileTypes: ["image/png", "image/jpeg", "application/pdf"],
},
},
};
return apps[slug] ?? null;
},
cors: {
allowedOrigins: ["http://localhost:5173", "https://example.com"],
},
});
export default {
fetch: (request: Request) => app.fetch(request),
};Cloudflare Workers Deployment
1. Create wrangler.toml
name = "dify-widget-backend"
main = "src/worker.ts"
compatibility_date = "2024-11-01"
compatibility_flags = ["nodejs_compat"]
[vars]
CORS_ORIGINS = "http://localhost:5173,http://localhost:3000"2. Set the SLUG_CONFIGS secret
Store your Dify API keys and slug configurations as a JSON secret:
wrangler secret put SLUG_CONFIGSEnter the following JSON when prompted:
{
"la-antara": {
"apiKey": "app-xxxxxxxxxxxx",
"baseUrl": "https://api.dify.ai/v1",
"enabled": true,
"allowedOrigins": ["https://your-domain.com"],
"publicConfig": {
"displayName": "La Antaraa Assistant",
"welcomeMessage": "How can I help you?",
"logoUrl": "",
"primaryColor": "#008F83",
"attachmentsEnabled": true,
"maxFiles": 3,
"maxFileSize": 10,
"allowedFileTypes": ["image/png", "image/jpeg", "application/pdf"]
}
}
}3. Set CORS origins
wrangler secret put CORS_ORIGINSEnter comma-separated origins: https://your-domain.com,https://staging.example.com
4. Deploy
wrangler deploy5. Local development
Create .dev.vars in the project root:
SLUG_CONFIGS={"la-antara":{"apiKey":"app-your-key","baseUrl":"https://api.dify.ai/v1","enabled":true,"publicConfig":{"displayName":"La Antaraa Assistant","welcomeMessage":"How can I help you?","logoUrl":"","primaryColor":"#008F83","attachmentsEnabled":true,"maxFiles":3,"maxFileSize":10,"allowedFileTypes":["image/png","image/jpeg","application/pdf"]}}}
CORS_ORIGINS=http://localhost:5173npx wrangler devAPI Routes
| Method | Route | Description |
|--------|-------|-------------|
| POST | /:slug/initialize | Get public widget config (no auth needed) |
| POST | /:slug/chat | Chat with streaming |
| POST | /:slug/files/upload | Upload a file |
| POST | /:slug/stop | Stop generation |
| POST | /:slug/feedback | Send message feedback |
| GET | /:slug/conversations | List conversations |
| GET | /health | Health check |
Options
{
resolveApp: ResolveDifyApp; // Required: slug → Dify config
cors?: CorsConfig; // CORS configuration
rateLimit?: RateLimitConfig; // Rate limiting
auth?: AuthConfig; // Custom auth middleware
logger?: Logger; // Custom logger
maxRequestBodyBytes?: number; // Max request size (default: 1MB)
requestTimeoutMs?: number; // Upstream timeout (default: 60s)
}CORS Configuration
cors: {
allowedOrigins: [
"http://localhost:5173",
"https://example.com",
],
allowCredentials: false,
allowedMethods: ["POST", "GET", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
maxAge: 86400,
}Per-slug origins override global origins:
resolveApp: async (slug) => {
if (slug === "restricted") {
return {
apiKey: "...",
baseUrl: "...",
allowedOrigins: ["https://specific-domain.com"],
};
}
}Frontend Integration
Using the widget
import { DifyChatWidget } from "@peacock/dify-chat-widget";
<DifyChatWidget
config={{
proxyUrl: "https://your-worker.workers.dev",
slug: "la-antara",
brand: { name: "My Assistant" },
// ... other config
}}
/>Using ProxyTransport directly
import { ProxyTransport } from "@peacock/dify-chat-widget/runtime";
const transport = new ProxyTransport({
proxyUrl: "https://your-worker.workers.dev",
slug: "la-antara",
});
// Get public config
const { widget } = await transport.initialize();
// Send a message (streams tokens via callbacks)
await transport.sendMessage(
{ query: "Hello!", slug: "la-antara", conversationId: "", userId: "visitor-1", inputs: {}, files: [] },
{ onStart: () => {}, onToken: (t) => console.log(t), onMessageEnd: () => {}, onFile: () => {}, onError: (e) => console.error(e), onPing: () => {}, onMessage: () => {} },
);React Demo Setup
# .env
VITE_DIFY_PROXY_URL=https://your-worker.workers.dev
VITE_DIFY_SLUG=la-antaraSlug Resolver Pattern
The resolveApp function maps public slugs to private Dify configurations. The API key is never exposed to the client.
Environment-based resolver
resolveApp: async (slug, { env }) => {
const e = env as Record<string, string>;
const key = `DIFY_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`;
const base = `DIFY_${slug.toUpperCase().replace(/-/g, "_")}_BASE_URL`;
if (!e[key] || !e[base]) return null;
return { apiKey: e[key], baseUrl: e[base] };
}JSON config resolver (used by the worker)
The default worker reads a SLUG_CONFIGS secret containing a JSON object mapping slugs to their configurations.
Security
- API keys are never exposed to the client
- Slugs are validated against
^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ - Origins are explicitly approved (no wildcard reflection)
- Request body size limits enforced
- Rate limiting available
- Upstream errors normalized (no secret leakage)
- Auth middleware hook available
Testing
npm test
npm run typecheck
npm run lintLicense
MIT
