From c8a8ea007da52bbccc6e819c13dd545f738d788c Mon Sep 17 00:00:00 2001 From: christiankrag Date: Mon, 15 Dec 2025 05:42:45 +0100 Subject: [PATCH] Initial wws serverless functions setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Dockerfile | 11 ++++++ README.md | 45 +++++++++++++++++++++- api/index.js | 46 ++++++++++++++++++++++ chat/index.js | 71 ++++++++++++++++++++++++++++++++++ webhook/index.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++++ wws.toml | 12 ++++++ 6 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 Dockerfile create mode 100644 api/index.js create mode 100644 chat/index.js create mode 100644 webhook/index.js create mode 100644 wws.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e0affd6 --- /dev/null +++ b/Dockerfile @@ -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", "."] diff --git a/README.md b/README.md index 305ea6e..68ce362 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ -# wws-functions +# Mylder WebAssembly Serverless Functions -Mylder WebAssembly Serverless Functions \ No newline at end of file +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. diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..037a314 --- /dev/null +++ b/api/index.js @@ -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 + } + }) +} diff --git a/chat/index.js b/chat/index.js new file mode 100644 index 0000000..6e5e3e5 --- /dev/null +++ b/chat/index.js @@ -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 } + }) + } +} diff --git a/webhook/index.js b/webhook/index.js new file mode 100644 index 0000000..6085acc --- /dev/null +++ b/webhook/index.js @@ -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 } + }) +} diff --git a/wws.toml b/wws.toml new file mode 100644 index 0000000..f0f847b --- /dev/null +++ b/wws.toml @@ -0,0 +1,12 @@ +version = "1" + +[server] +host = "0.0.0.0" +port = 8080 + +[[folders]] +from = "." +to = "/" + +[features] +environment_variables = true