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:
2025-12-15 05:42:45 +01:00
parent 4aba1199fa
commit c8a8ea007d
6 changed files with 282 additions and 2 deletions

11
Dockerfile Normal file
View 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", "."]

View File

@@ -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
View 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
View 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
View 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 }
})
}

12
wws.toml Normal file
View File

@@ -0,0 +1,12 @@
version = "1"
[server]
host = "0.0.0.0"
port = 8080
[[folders]]
from = "."
to = "/"
[features]
environment_variables = true