Initial wws serverless functions setup
- Add health check API endpoint (/api) - Add chat proxy to n8n workflow (/chat) - Add webhook receiver for external integrations (/webhook) - Add Dockerfile for container deployment - Add wws.toml configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM ghcr.io/vmware-labs/wws:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy all functions
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# wws automatically serves files from /app
|
||||||
|
CMD ["wws", "."]
|
||||||
45
README.md
45
README.md
@@ -1,3 +1,44 @@
|
|||||||
# wws-functions
|
# Mylder WebAssembly Serverless Functions
|
||||||
|
|
||||||
Mylder WebAssembly Serverless Functions
|
Self-hosted serverless functions using [wasm-workers-server](https://github.com/vmware-labs/wasm-workers-server). Cloudflare Workers compatible API with zero vendor lock-in.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Path | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `/api` | Health check and service info |
|
||||||
|
| `/chat` | Chat proxy to n8n AI workflow |
|
||||||
|
| `/webhook` | Webhook receiver for external integrations |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Chat API
|
||||||
|
```bash
|
||||||
|
curl -X POST https://wws.mylder.io/chat \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message": "Help me brainstorm ideas", "provider": "zai"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://wws.mylder.io/webhook?source=gitea" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"event": "push", "repo": "mylder-frontend"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install wws
|
||||||
|
curl -fsSL https://workers.wasmlabs.dev/install | bash
|
||||||
|
|
||||||
|
# Run locally
|
||||||
|
wws .
|
||||||
|
|
||||||
|
# Test
|
||||||
|
curl http://localhost:8080/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Deployed via Dokploy with auto-deploy from Gitea on push to main branch.
|
||||||
|
|||||||
46
api/index.js
Normal file
46
api/index.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Health check and API info endpoint
|
||||||
|
// Path: /api
|
||||||
|
|
||||||
|
addEventListener('fetch', event => {
|
||||||
|
event.respondWith(handleRequest(event.request))
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleRequest(request) {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
|
||||||
|
// CORS headers for browser requests
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
status: 'ok',
|
||||||
|
service: 'mylder-wws',
|
||||||
|
version: '1.0.0',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
endpoints: {
|
||||||
|
health: '/api',
|
||||||
|
chat: '/chat',
|
||||||
|
webhook: '/webhook'
|
||||||
|
},
|
||||||
|
runtime: 'wasm-workers-server',
|
||||||
|
features: [
|
||||||
|
'cloudflare-compatible',
|
||||||
|
'zero-vendor-lock-in',
|
||||||
|
'webassembly-sandbox'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(response, null, 2), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
71
chat/index.js
Normal file
71
chat/index.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Chat proxy - routes requests to n8n workflow
|
||||||
|
// Path: /chat
|
||||||
|
|
||||||
|
addEventListener('fetch', event => {
|
||||||
|
event.respondWith(handleRequest(event.request))
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleRequest(request) {
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method !== 'POST') {
|
||||||
|
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
||||||
|
status: 405,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { message, project_id, user_id, provider = 'zai' } = body
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Message is required' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to n8n webhook
|
||||||
|
const n8nResponse = await fetch('https://n8n.mylder.io/webhook/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
message,
|
||||||
|
project_id: project_id || null,
|
||||||
|
user_id: user_id || null,
|
||||||
|
provider,
|
||||||
|
source: 'wws-proxy',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await n8nResponse.json()
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
response: result.response || result,
|
||||||
|
provider,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Internal server error'
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
99
webhook/index.js
Normal file
99
webhook/index.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Webhook receiver for external integrations
|
||||||
|
// Path: /webhook
|
||||||
|
|
||||||
|
addEventListener('fetch', event => {
|
||||||
|
event.respondWith(handleRequest(event.request))
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleRequest(request) {
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Webhook-Secret',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const path = url.pathname
|
||||||
|
|
||||||
|
// Log incoming webhook
|
||||||
|
const logEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: request.method,
|
||||||
|
path: path,
|
||||||
|
headers: Object.fromEntries(request.headers.entries()),
|
||||||
|
query: Object.fromEntries(url.searchParams.entries())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const contentType = request.headers.get('content-type') || ''
|
||||||
|
|
||||||
|
let payload
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
payload = await request.json()
|
||||||
|
} else if (contentType.includes('form')) {
|
||||||
|
payload = Object.fromEntries(await request.formData())
|
||||||
|
} else {
|
||||||
|
payload = await request.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry.payload = payload
|
||||||
|
|
||||||
|
// Route based on webhook type/source
|
||||||
|
const source = url.searchParams.get('source') || 'unknown'
|
||||||
|
|
||||||
|
// Forward to appropriate n8n workflow based on source
|
||||||
|
const webhookMap = {
|
||||||
|
'gitea': 'https://n8n.mylder.io/webhook/gitea-event',
|
||||||
|
'stripe': 'https://n8n.mylder.io/webhook/stripe-event',
|
||||||
|
'supabase': 'https://n8n.mylder.io/webhook/supabase-event',
|
||||||
|
'default': 'https://n8n.mylder.io/webhook/generic-event'
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUrl = webhookMap[source] || webhookMap['default']
|
||||||
|
|
||||||
|
// Async forward (fire and forget for speed)
|
||||||
|
fetch(targetUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
source,
|
||||||
|
payload,
|
||||||
|
received_at: logEntry.timestamp,
|
||||||
|
headers: logEntry.headers
|
||||||
|
})
|
||||||
|
}).catch(err => console.error('Forward failed:', err))
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
received: true,
|
||||||
|
source,
|
||||||
|
timestamp: logEntry.timestamp
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
received: false,
|
||||||
|
error: error.message
|
||||||
|
}), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET request - return webhook info
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
endpoint: '/webhook',
|
||||||
|
method: 'POST',
|
||||||
|
supported_sources: ['gitea', 'stripe', 'supabase', 'default'],
|
||||||
|
usage: 'POST /webhook?source=gitea with JSON body'
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user