Replace server.js with thin entry pointing at compiled TS output
This commit is contained in:
parent
d1fe43302b
commit
1d899a3f18
158
server.js
158
server.js
@ -1,155 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* bside-webhook-proxy
|
* Entry point. The actual source lives in src/server.ts and is compiled
|
||||||
|
* to dist/server.js by the TypeScript build step (see package.json "build").
|
||||||
*
|
*
|
||||||
* Multi-project webhook proxy for incoming webhooks (Square, Stripe, GitHub, etc.).
|
* Run the compiled output via:
|
||||||
* Receives webhooks that can't send custom auth headers and forwards them to the
|
* npm run build && npm start
|
||||||
* appropriate Appwrite function with X-Appwrite-Project + X-Appwrite-Key headers.
|
|
||||||
*
|
*
|
||||||
* Routing is resolved via querystring params OR a short-path route map loaded
|
* Or directly with tsx during development:
|
||||||
* from the ROUTE_MAP environment variable.
|
* npx tsx src/server.ts
|
||||||
*
|
|
||||||
* Required env vars:
|
|
||||||
* PROJECT_CREDENTIALS JSON object mapping slot name -> { project, key }
|
|
||||||
* e.g. {"square":{"project":"abc","key":"def"}}
|
|
||||||
*
|
|
||||||
* Optional env vars:
|
|
||||||
* ROUTE_MAP String mapping short paths to { apiKeySlot, functionId }
|
|
||||||
* e.g. /square=>{"apiKeySlot":"square","functionId":"verify_square_webhook"}
|
|
||||||
* APPWRITE_BASE_URL Base URL for the Appwrite API
|
|
||||||
* (default: https://appwrite.bsidesolutions.net/v1)
|
|
||||||
* PORT Port to listen on (default: 3012)
|
|
||||||
*/
|
*/
|
||||||
|
require('./dist/server.js');
|
||||||
const express = require('express');
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Capture raw request body for all content types so we can forward it as-is.
|
|
||||||
// This is important for services like Square that use signature validation
|
|
||||||
// based on the raw payload bytes.
|
|
||||||
app.use(express.text({ type: '*/*', limit: '1mb' }));
|
|
||||||
|
|
||||||
// ----- Config ---------------------------------------------------------------
|
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || '3012', 10);
|
|
||||||
const APPWRITE_BASE_URL = (process.env.APPWRITE_BASE_URL || 'https://appwrite.bsidesolutions.net/v1').replace(/\/$/, '');
|
|
||||||
|
|
||||||
let PROJECT_CREDENTIALS = {};
|
|
||||||
try {
|
|
||||||
PROJECT_CREDENTIALS = JSON.parse(process.env.PROJECT_CREDENTIALS || '{}');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[startup] PROJECT_CREDENTIALS is not valid JSON:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse ROUTE_MAP. Format: "/path=>{json};/path2=>{json2}"
|
|
||||||
const ROUTE_MAP = {};
|
|
||||||
if (process.env.ROUTE_MAP) {
|
|
||||||
for (const entry of process.env.ROUTE_MAP.split(';')) {
|
|
||||||
const trimmed = entry.trim();
|
|
||||||
if (!trimmed) continue;
|
|
||||||
const arrowIdx = trimmed.indexOf('=>');
|
|
||||||
if (arrowIdx === -1) continue;
|
|
||||||
const path = trimmed.slice(0, arrowIdx).trim();
|
|
||||||
const cfgStr = trimmed.slice(arrowIdx + 2).trim();
|
|
||||||
try {
|
|
||||||
ROUTE_MAP[path] = JSON.parse(cfgStr);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[startup] Invalid ROUTE_MAP entry for "${path}":`, err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[startup] Listening on port ${PORT}`);
|
|
||||||
console.log(`[startup] Appwrite base URL: ${APPWRITE_BASE_URL}`);
|
|
||||||
console.log(`[startup] Loaded ${Object.keys(PROJECT_CREDENTIALS).length} project credential slot(s)`);
|
|
||||||
console.log(`[startup] Loaded ${Object.keys(ROUTE_MAP).length} short-path route(s)`);
|
|
||||||
|
|
||||||
// ----- Health check ---------------------------------------------------------
|
|
||||||
|
|
||||||
app.get('/health', (_req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
projects: Object.keys(PROJECT_CREDENTIALS),
|
|
||||||
routes: Object.keys(ROUTE_MAP),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Webhook receiver -----------------------------------------------------
|
|
||||||
|
|
||||||
app.post('*', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
||||||
const projectParam = url.searchParams.get('project');
|
|
||||||
const functionParam = url.searchParams.get('function');
|
|
||||||
const apiKeyParam = url.searchParams.get('apiKey');
|
|
||||||
const pathRoute = url.pathname;
|
|
||||||
|
|
||||||
// Resolve project/function/apiKey. Explicit querystring params take priority,
|
|
||||||
// then short-path route map (e.g. POST /square).
|
|
||||||
let resolvedProject = projectParam;
|
|
||||||
let resolvedFunction = functionParam;
|
|
||||||
let resolvedSlot = apiKeyParam;
|
|
||||||
|
|
||||||
if (!resolvedProject && ROUTE_MAP[pathRoute]) {
|
|
||||||
const route = ROUTE_MAP[pathRoute];
|
|
||||||
resolvedSlot = route.apiKeySlot;
|
|
||||||
resolvedFunction = route.functionId;
|
|
||||||
resolvedProject = PROJECT_CREDENTIALS[resolvedSlot]?.project;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resolvedProject || !resolvedFunction || !resolvedSlot) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Missing required params',
|
|
||||||
required: ['project', 'function', 'apiKey'],
|
|
||||||
or: 'POST to a configured short-path route (e.g. /square)',
|
|
||||||
available_routes: Object.keys(ROUTE_MAP),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentials = PROJECT_CREDENTIALS[resolvedSlot];
|
|
||||||
if (!credentials || credentials.project !== resolvedProject) {
|
|
||||||
return res.status(403).json({ error: 'Unknown project or apiKey slot' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body || {});
|
|
||||||
const contentType = req.headers['content-type'] || 'application/json';
|
|
||||||
|
|
||||||
const appwriteUrl = `${APPWRITE_BASE_URL}/functions/${resolvedFunction}/executions`;
|
|
||||||
console.log(`[webhook] slot=${resolvedSlot} project=${resolvedProject} function=${resolvedFunction} -> ${appwriteUrl}`);
|
|
||||||
|
|
||||||
const appwriteResp = await fetch(appwriteUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-Appwrite-Project': credentials.project,
|
|
||||||
'X-Appwrite-Key': credentials.key,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
body: rawBody,
|
|
||||||
async: false,
|
|
||||||
method: 'POST',
|
|
||||||
path: '/webhook',
|
|
||||||
headers: { 'content-type': contentType },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await appwriteResp.text();
|
|
||||||
res.status(appwriteResp.status).type('application/json').send(responseText);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[webhook] Error forwarding webhook:', err);
|
|
||||||
res.status(500).json({ error: 'Internal proxy error', detail: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- 404 ------------------------------------------------------------------
|
|
||||||
|
|
||||||
app.use((_req, res) => {
|
|
||||||
res.status(404).json({ error: 'Not found. POST webhooks to this proxy with the right params.' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Start ----------------------------------------------------------------
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
// The startup banner already printed above.
|
|
||||||
});
|
|
||||||
Loading…
x
Reference in New Issue
Block a user