Add ECDSA signature verification and enhance ComboHandler functionality

- Integrated ECDSA for signature verification in ComboHandler.
- Updated event emissions to include additional parameters for better tracking.
- Improved gas tracking during execution of combo plans.
- Enhanced database interactions for storing and retrieving plans, including conflict resolution and status updates.
- Added new dependencies for security and database management in orchestrator.
This commit is contained in:
defiQUG
2025-11-05 16:28:48 -08:00
parent 3b09c35c47
commit f600b7b15e
48 changed files with 3381 additions and 46 deletions

View File

@@ -0,0 +1,49 @@
import { Request, Response } from "express";
import { executionCoordinator } from "../services/execution";
import { asyncHandler } from "../services/errorHandler";
import { auditLog } from "../middleware";
/**
* POST /api/plans/:planId/execute
* Execute a plan
*/
export const executePlan = asyncHandler(async (req: Request, res: Response) => {
const { planId } = req.params;
const result = await executionCoordinator.executePlan(planId);
res.json(result);
});
/**
* GET /api/plans/:planId/status
* Get execution status
*/
export const getExecutionStatus = asyncHandler(async (req: Request, res: Response) => {
const { planId } = req.params;
const executionId = req.query.executionId as string;
if (executionId) {
const status = await executionCoordinator.getExecutionStatus(executionId);
return res.json(status);
}
// Get latest execution for plan
res.json({ status: "pending" });
});
/**
* POST /api/plans/:planId/abort
* Abort execution
*/
export const abortExecution = asyncHandler(async (req: Request, res: Response) => {
const { planId } = req.params;
const executionId = req.query.executionId as string;
if (executionId) {
await executionCoordinator.abortExecution(executionId, planId, "User aborted");
}
res.json({ success: true });
});

View File

@@ -0,0 +1,38 @@
import { Router } from "express";
import swaggerUi from "swagger-ui-express";
import swaggerJsdoc from "swagger-jsdoc";
const options: swaggerJsdoc.Options = {
definition: {
openapi: "3.0.0",
info: {
title: "ISO-20022 Combo Flow Orchestrator API",
version: "1.0.0",
description: "API for managing and executing financial workflow plans",
},
servers: [
{
url: "http://localhost:8080",
description: "Development server",
},
],
components: {
securitySchemes: {
ApiKeyAuth: {
type: "apiKey",
in: "header",
name: "X-API-Key",
},
},
},
},
apis: ["./src/api/**/*.ts"],
};
const specs = swaggerJsdoc(options);
export function setupSwagger(router: Router) {
router.use("/api-docs", swaggerUi.serve);
router.get("/api-docs", swaggerUi.setup(specs));
}

View File

@@ -0,0 +1,22 @@
import { Router } from "express";
/**
* API versioning middleware
*/
export function apiVersion(version: string) {
return (req: any, res: any, next: any) => {
req.apiVersion = version;
res.setHeader("API-Version", version);
next();
};
}
/**
* Create versioned router
*/
export function createVersionedRouter(version: string) {
const router = Router();
router.use(apiVersion(version));
return router;
}

View File

@@ -0,0 +1,78 @@
import { Request, Response } from "express";
import { executionCoordinator } from "../services/execution";
import { logger } from "../logging/logger";
interface WebhookConfig {
url: string;
secret: string;
events: string[];
}
const webhooks: Map<string, WebhookConfig> = new Map();
/**
* POST /api/webhooks
* Register a webhook
*/
export async function registerWebhook(req: Request, res: Response) {
try {
const { url, secret, events } = req.body;
if (!url || !secret || !events || !Array.isArray(events)) {
return res.status(400).json({
error: "Invalid webhook configuration",
});
}
const webhookId = `webhook-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
webhooks.set(webhookId, { url, secret, events });
res.json({ webhookId, url, events });
} catch (error: any) {
logger.error({ error }, "Failed to register webhook");
res.status(500).json({ error: error.message });
}
}
/**
* Send webhook notification
*/
export async function sendWebhook(event: string, payload: any) {
for (const [webhookId, config] of webhooks.entries()) {
if (config.events.includes(event) || config.events.includes("*")) {
try {
const signature = createWebhookSignature(JSON.stringify(payload), config.secret);
await fetch(config.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Event": event,
"X-Webhook-Signature": signature,
"X-Webhook-Id": webhookId,
},
body: JSON.stringify(payload),
});
} catch (error) {
logger.error({ error, webhookId, event }, "Failed to send webhook");
}
}
}
}
/**
* Create webhook signature
*/
function createWebhookSignature(payload: string, secret: string): string {
const crypto = require("crypto");
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
}
// Listen to execution events
executionCoordinator.onStatus((executionId, event) => {
sendWebhook("plan.status", {
executionId,
...event,
});
});

View File

@@ -0,0 +1,57 @@
import { z } from "zod";
/**
* Environment variable validation schema
*/
const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.string().transform(Number).pipe(z.number().int().positive()),
DATABASE_URL: z.string().url().optional(),
API_KEYS: z.string().optional(),
REDIS_URL: z.string().url().optional(),
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
ALLOWED_IPS: z.string().optional(),
SESSION_SECRET: z.string().min(32),
JWT_SECRET: z.string().min(32).optional(),
AZURE_KEY_VAULT_URL: z.string().url().optional(),
AWS_SECRETS_MANAGER_REGION: z.string().optional(),
SENTRY_DSN: z.string().url().optional(),
});
/**
* Validated environment variables
*/
export const env = envSchema.parse({
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT || "8080",
DATABASE_URL: process.env.DATABASE_URL,
API_KEYS: process.env.API_KEYS,
REDIS_URL: process.env.REDIS_URL,
LOG_LEVEL: process.env.LOG_LEVEL,
ALLOWED_IPS: process.env.ALLOWED_IPS,
SESSION_SECRET: process.env.SESSION_SECRET || "dev-secret-change-in-production-min-32-chars",
JWT_SECRET: process.env.JWT_SECRET,
AZURE_KEY_VAULT_URL: process.env.AZURE_KEY_VAULT_URL,
AWS_SECRETS_MANAGER_REGION: process.env.AWS_SECRETS_MANAGER_REGION,
SENTRY_DSN: process.env.SENTRY_DSN,
});
/**
* Validate environment on startup
*/
export function validateEnv() {
try {
envSchema.parse(process.env);
console.log("✅ Environment variables validated");
} catch (error) {
if (error instanceof z.ZodError) {
console.error("❌ Environment validation failed:");
error.errors.forEach((err) => {
console.error(` - ${err.path.join(".")}: ${err.message}`);
});
process.exit(1);
}
throw error;
}
}

View File

@@ -0,0 +1,47 @@
import { query } from "../postgres";
import fs from "fs";
import path from "path";
/**
* Run initial database schema migration
*/
export async function up() {
const schemaPath = path.join(__dirname, "../schema.sql");
const schema = fs.readFileSync(schemaPath, "utf-8");
// Split by semicolons and execute each statement
const statements = schema
.split(";")
.map((s) => s.trim())
.filter((s) => s.length > 0 && !s.startsWith("--"));
for (const statement of statements) {
try {
await query(statement);
} catch (error: any) {
// Ignore "already exists" errors
if (!error.message.includes("already exists")) {
throw error;
}
}
}
console.log("✅ Database schema migrated successfully");
}
/**
* Rollback migration (not implemented for initial schema)
*/
export async function down() {
// Drop tables in reverse order
await query("DROP TABLE IF EXISTS compliance_status CASCADE");
await query("DROP TABLE IF EXISTS users CASCADE");
await query("DROP TABLE IF EXISTS audit_logs CASCADE");
await query("DROP TABLE IF EXISTS receipts CASCADE");
await query("DROP TABLE IF EXISTS executions CASCADE");
await query("DROP TABLE IF EXISTS plans CASCADE");
await query("DROP FUNCTION IF EXISTS update_updated_at_column CASCADE");
console.log("✅ Database schema rolled back");
}

View File

@@ -0,0 +1,15 @@
import { up as up001 } from "./001_initial_schema";
/**
* Run all migrations
*/
export async function runMigration() {
try {
await up001();
console.log("✅ All migrations completed");
} catch (error) {
console.error("❌ Migration failed:", error);
throw error;
}
}

View File

@@ -1,29 +1,101 @@
// In-memory database for plans (mock implementation)
// In production, replace with actual database (PostgreSQL, MongoDB, etc.)
import { query, transaction } from "./postgres";
import type { Plan } from "../types/plan";
const plans: Map<string, any> = new Map();
export async function storePlan(plan: any): Promise<void> {
plans.set(plan.plan_id, plan);
/**
* Store plan in database
*/
export async function storePlan(plan: Plan): Promise<void> {
await query(
`INSERT INTO plans (
plan_id, creator, plan_hash, steps, max_recursion, max_ltv,
signature, message_hash, signer_address, signed_at, status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (plan_id) DO UPDATE SET
steps = EXCLUDED.steps,
status = EXCLUDED.status,
updated_at = CURRENT_TIMESTAMP`,
[
plan.plan_id,
plan.creator,
plan.plan_hash,
JSON.stringify(plan.steps),
plan.maxRecursion || 3,
plan.maxLTV || 0.6,
plan.signature || null,
null, // message_hash
null, // signer_address
null, // signed_at
plan.status || "pending",
]
);
}
export async function getPlanById(planId: string): Promise<any | null> {
return plans.get(planId) || null;
}
/**
* Get plan by ID
*/
export async function getPlanById(planId: string): Promise<Plan | null> {
const result = await query<Plan>(
"SELECT * FROM plans WHERE plan_id = $1",
[planId]
);
export async function updatePlanSignature(planId: string, signature: any): Promise<void> {
const plan = plans.get(planId);
if (plan) {
plan.signature = signature;
plans.set(planId, plan);
if (result.length === 0) {
return null;
}
const row = result[0];
return {
plan_id: row.plan_id,
creator: row.creator,
steps: typeof row.steps === "string" ? JSON.parse(row.steps) : row.steps,
maxRecursion: row.max_recursion,
maxLTV: row.max_ltv,
signature: row.signature,
plan_hash: row.plan_hash,
created_at: row.created_at?.toISOString(),
status: row.status,
};
}
export async function updatePlanStatus(planId: string, status: string): Promise<void> {
const plan = plans.get(planId);
if (plan) {
plan.status = status;
plans.set(planId, plan);
/**
* Update plan signature
*/
export async function updatePlanSignature(
planId: string,
signature: {
signature: string;
messageHash: string;
signerAddress: string;
signedAt: string;
}
): Promise<void> {
await query(
`UPDATE plans SET
signature = $1,
message_hash = $2,
signer_address = $3,
signed_at = $4,
updated_at = CURRENT_TIMESTAMP
WHERE plan_id = $5`,
[
signature.signature,
signature.messageHash,
signature.signerAddress,
signature.signedAt,
planId,
]
);
}
/**
* Update plan status
*/
export async function updatePlanStatus(
planId: string,
status: string
): Promise<void> {
await query(
"UPDATE plans SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE plan_id = $2",
[status, planId]
);
}

View File

@@ -0,0 +1,94 @@
import { Pool, PoolClient } from "pg";
import { env } from "../config/env";
/**
* PostgreSQL connection pool
*/
let pool: Pool | null = null;
/**
* Get database connection pool
*/
export function getPool(): Pool {
if (!pool) {
pool = new Pool({
connectionString: env.DATABASE_URL || "postgresql://user:pass@localhost:5432/comboflow",
max: 20, // Maximum number of clients in the pool
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on("error", (err) => {
console.error("Unexpected error on idle client", err);
});
}
return pool;
}
/**
* Execute query with automatic retry
*/
export async function query<T = any>(
text: string,
params?: any[],
retries = 3
): Promise<T[]> {
const pool = getPool();
let lastError: Error | null = null;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const result = await pool.query(text, params);
return result.rows as T[];
} catch (error: any) {
lastError = error;
// Don't retry on certain errors
if (error.code === "23505" || error.code === "23503") {
throw error;
}
if (attempt < retries) {
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
console.log(`Database query retry ${attempt + 1}/${retries}`);
}
}
}
throw lastError || new Error("Database query failed after retries");
}
/**
* Execute transaction
*/
export async function transaction<T>(
callback: (client: PoolClient) => Promise<T>
): Promise<T> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await callback(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* Close database connections
*/
export async function closePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
}

View File

@@ -0,0 +1,139 @@
-- Database schema for ISO-20022 Combo Flow Orchestrator
-- Plans table
CREATE TABLE IF NOT EXISTS plans (
plan_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator VARCHAR(255) NOT NULL,
plan_hash VARCHAR(64) NOT NULL UNIQUE,
steps JSONB NOT NULL,
max_recursion INTEGER DEFAULT 3,
max_ltv DECIMAL(5,2) DEFAULT 0.60,
signature TEXT,
message_hash VARCHAR(64),
signer_address VARCHAR(42),
signed_at TIMESTAMP,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_plans_creator ON plans(creator);
CREATE INDEX idx_plans_status ON plans(status);
CREATE INDEX idx_plans_created_at ON plans(created_at);
CREATE INDEX idx_plans_plan_hash ON plans(plan_hash);
-- Executions table
CREATE TABLE IF NOT EXISTS executions (
execution_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plan_id UUID NOT NULL REFERENCES plans(plan_id) ON DELETE CASCADE,
status VARCHAR(20) DEFAULT 'pending',
phase VARCHAR(50),
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
error TEXT,
dlt_tx_hash VARCHAR(66),
iso_message_id VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_executions_plan_id ON executions(plan_id);
CREATE INDEX idx_executions_status ON executions(status);
CREATE INDEX idx_executions_started_at ON executions(started_at);
-- Receipts table
CREATE TABLE IF NOT EXISTS receipts (
receipt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plan_id UUID NOT NULL REFERENCES plans(plan_id) ON DELETE CASCADE,
execution_id UUID REFERENCES executions(execution_id),
receipt_hash VARCHAR(64) NOT NULL UNIQUE,
dlt_transaction JSONB,
iso_message JSONB,
notary_proof JSONB,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_receipts_plan_id ON receipts(plan_id);
CREATE INDEX idx_receipts_receipt_hash ON receipts(receipt_hash);
-- Audit logs table
CREATE TABLE IF NOT EXISTS audit_logs (
log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
request_id VARCHAR(255),
user_id VARCHAR(255),
action VARCHAR(100) NOT NULL,
resource VARCHAR(255) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
success BOOLEAN DEFAULT true,
error_message TEXT,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
CREATE INDEX idx_audit_logs_request_id ON audit_logs(request_id);
-- Users/Identities table
CREATE TABLE IF NOT EXISTS users (
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
lei VARCHAR(20),
did VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_lei ON users(lei);
-- Compliance status table
CREATE TABLE IF NOT EXISTS compliance_status (
compliance_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
lei VARCHAR(20),
did VARCHAR(255),
kyc_level INTEGER DEFAULT 0,
kyc_verified BOOLEAN DEFAULT false,
kyc_expires_at TIMESTAMP,
aml_passed BOOLEAN DEFAULT false,
aml_last_check TIMESTAMP,
aml_risk_level VARCHAR(20),
valid BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_compliance_user_id ON compliance_status(user_id);
CREATE INDEX idx_compliance_valid ON compliance_status(valid);
CREATE INDEX idx_compliance_kyc_expires ON compliance_status(kyc_expires_at);
-- Update timestamp trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply update triggers
CREATE TRIGGER update_plans_updated_at BEFORE UPDATE ON plans
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_executions_updated_at BEFORE UPDATE ON executions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_receipts_updated_at BEFORE UPDATE ON receipts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_compliance_status_updated_at BEFORE UPDATE ON compliance_status
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,78 @@
import { getPool } from "../db/postgres";
interface HealthStatus {
status: "healthy" | "unhealthy";
timestamp: string;
checks: {
database: "up" | "down";
memory: "ok" | "warning" | "critical";
disk: "ok" | "warning" | "critical";
};
uptime: number;
version: string;
}
/**
* Health check endpoint
*/
export async function healthCheck(): Promise<HealthStatus> {
const startTime = Date.now();
const checks: HealthStatus["checks"] = {
database: "down",
memory: "ok",
disk: "ok",
};
// Check database
try {
const pool = getPool();
await pool.query("SELECT 1");
checks.database = "up";
} catch (error) {
checks.database = "down";
}
// Check memory usage
const memUsage = process.memoryUsage();
const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
if (memUsagePercent > 90) {
checks.memory = "critical";
} else if (memUsagePercent > 75) {
checks.memory = "warning";
}
// Check disk space (mock - in production use actual disk stats)
checks.disk = "ok";
const allHealthy = checks.database === "up" && checks.memory !== "critical" && checks.disk !== "critical";
return {
status: allHealthy ? "healthy" : "unhealthy",
timestamp: new Date().toISOString(),
checks,
uptime: Date.now() - startTime,
version: process.env.npm_package_version || "1.0.0",
};
}
/**
* Readiness check (for Kubernetes)
*/
export async function readinessCheck(): Promise<boolean> {
try {
const pool = getPool();
await pool.query("SELECT 1");
return true;
} catch {
return false;
}
}
/**
* Liveness check (for Kubernetes)
*/
export async function livenessCheck(): Promise<boolean> {
// Simple check - if process is running, we're alive
return true;
}

139
orchestrator/src/index.ts Normal file
View File

@@ -0,0 +1,139 @@
import express from "express";
import cors from "cors";
import { validateEnv } from "./config/env";
import {
apiLimiter,
securityHeaders,
requestSizeLimits,
requestId,
apiKeyAuth,
auditLog,
} from "./middleware";
import { logger } from "./logging/logger";
import { getMetrics, httpRequestDuration, httpRequestTotal, register } from "./metrics/prometheus";
import { healthCheck, readinessCheck, livenessCheck } from "./health/health";
import { createPlan, getPlan, addSignature, validatePlanEndpoint } from "./api/plans";
import { streamPlanStatus } from "./api/sse";
import { executionCoordinator } from "./services/execution";
import { runMigration } from "./db/migrations";
// Validate environment on startup
validateEnv();
const app = express();
const PORT = process.env.PORT || 8080;
// Middleware
app.use(cors());
app.use(securityHeaders);
app.use(requestSizeLimits);
app.use(requestId);
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
const requestId = req.headers["x-request-id"] as string || "unknown";
res.on("finish", () => {
const duration = Date.now() - start;
httpRequestDuration.observe(
{ method: req.method, route: req.route?.path || req.path, status: res.statusCode },
duration / 1000
);
httpRequestTotal.inc({ method: req.method, route: req.route?.path || req.path, status: res.statusCode });
logger.info({
req,
res,
duration,
requestId,
}, `${req.method} ${req.path} ${res.statusCode}`);
});
next();
});
// Health check endpoints (no auth required)
app.get("/health", async (req, res) => {
const health = await healthCheck();
res.status(health.status === "healthy" ? 200 : 503).json(health);
});
app.get("/ready", async (req, res) => {
const ready = await readinessCheck();
res.status(ready ? 200 : 503).json({ ready });
});
app.get("/live", async (req, res) => {
const alive = await livenessCheck();
res.status(alive ? 200 : 503).json({ alive });
});
// Metrics endpoint
app.get("/metrics", async (req, res) => {
res.setHeader("Content-Type", register.contentType);
const metrics = await getMetrics();
res.send(metrics);
});
// API routes with rate limiting
app.use("/api", apiLimiter);
// Plan management endpoints
app.post("/api/plans", auditLog("CREATE_PLAN", "plan"), createPlan);
app.get("/api/plans/:planId", getPlan);
app.post("/api/plans/:planId/signature", addSignature);
app.post("/api/plans/:planId/validate", validatePlanEndpoint);
// Execution endpoints
import { executePlan, getExecutionStatus, abortExecution } from "./api/execution";
app.post("/api/plans/:planId/execute", auditLog("EXECUTE_PLAN", "plan"), executePlan);
app.get("/api/plans/:planId/status", getExecutionStatus);
app.post("/api/plans/:planId/abort", auditLog("ABORT_PLAN", "plan"), abortExecution);
app.get("/api/plans/:planId/status/stream", streamPlanStatus);
// Error handling middleware
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error({ err, req }, "Unhandled error");
res.status(err.status || 500).json({
error: "Internal server error",
message: process.env.NODE_ENV === "development" ? err.message : undefined,
requestId: req.headers["x-request-id"],
});
});
// Graceful shutdown
process.on("SIGTERM", async () => {
logger.info("SIGTERM received, shutting down gracefully");
// Close database connections
// Close SSE connections
process.exit(0);
});
process.on("SIGINT", async () => {
logger.info("SIGINT received, shutting down gracefully");
process.exit(0);
});
// Start server
async function start() {
try {
// Run database migrations
if (process.env.RUN_MIGRATIONS === "true") {
await runMigration();
}
app.listen(PORT, () => {
logger.info({ port: PORT }, "Orchestrator service started");
});
} catch (error) {
logger.error({ error }, "Failed to start server");
process.exit(1);
}
}
start();

View File

@@ -0,0 +1,74 @@
import pino from "pino";
import { env } from "../config/env";
/**
* Configure Pino logger with structured logging
*/
export const logger = pino({
level: env.LOG_LEVEL,
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:standard",
ignore: "pid,hostname",
},
},
formatters: {
level: (label) => {
return { level: label };
},
},
serializers: {
req: (req) => ({
id: req.id,
method: req.method,
url: req.url,
headers: {
host: req.headers.host,
"user-agent": req.headers["user-agent"],
"x-request-id": req.headers["x-request-id"],
},
}),
res: (res) => ({
statusCode: res.statusCode,
}),
err: pino.stdSerializers.err,
},
});
/**
* Mask PII in log data
*/
export function maskPII(data: any): any {
if (typeof data === "string") {
// Mask email addresses
return data.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[EMAIL]");
}
if (Array.isArray(data)) {
return data.map(maskPII);
}
if (data && typeof data === "object") {
const masked: any = {};
for (const key in data) {
const lowerKey = key.toLowerCase();
if (lowerKey.includes("email") || lowerKey.includes("password") || lowerKey.includes("secret") || lowerKey.includes("token")) {
masked[key] = "[REDACTED]";
} else if (lowerKey.includes("iban") || lowerKey.includes("account")) {
masked[key] = data[key] ? `${String(data[key]).substring(0, 4)}****` : data[key];
} else {
masked[key] = maskPII(data[key]);
}
}
return masked;
}
return data;
}
/**
* Create child logger with context
*/
export function createChildLogger(context: Record<string, any>) {
return logger.child(maskPII(context));
}

View File

@@ -0,0 +1,79 @@
import { Registry, Counter, Histogram, Gauge } from "prom-client";
/**
* Prometheus metrics registry
*/
export const register = new Registry();
/**
* HTTP request metrics
*/
export const httpRequestDuration = new Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests in seconds",
labelNames: ["method", "route", "status"],
buckets: [0.1, 0.5, 1, 2, 5, 10],
registers: [register],
});
export const httpRequestTotal = new Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "route", "status"],
registers: [register],
});
/**
* Business metrics
*/
export const planCreationTotal = new Counter({
name: "plans_created_total",
help: "Total number of plans created",
labelNames: ["status"],
registers: [register],
});
export const planExecutionTotal = new Counter({
name: "plans_executed_total",
help: "Total number of plans executed",
labelNames: ["status"],
registers: [register],
});
export const planExecutionDuration = new Histogram({
name: "plan_execution_duration_seconds",
help: "Duration of plan execution in seconds",
labelNames: ["status"],
buckets: [1, 5, 10, 30, 60, 120],
registers: [register],
});
export const complianceCheckTotal = new Counter({
name: "compliance_checks_total",
help: "Total number of compliance checks",
labelNames: ["status"],
registers: [register],
});
/**
* System metrics
*/
export const activeExecutions = new Gauge({
name: "active_executions",
help: "Number of currently active plan executions",
registers: [register],
});
export const databaseConnections = new Gauge({
name: "database_connections",
help: "Number of active database connections",
registers: [register],
});
/**
* Get metrics endpoint handler
*/
export async function getMetrics(): Promise<string> {
return register.metrics();
}

View File

@@ -0,0 +1,44 @@
import { Request, Response, NextFunction } from "express";
/**
* API Key authentication middleware
*/
export const apiKeyAuth = (req: Request, res: Response, next: NextFunction) => {
const apiKey = req.headers["x-api-key"] || req.headers["authorization"]?.replace("Bearer ", "");
if (!apiKey) {
return res.status(401).json({
error: "Unauthorized",
message: "API key is required",
});
}
// Validate API key (in production, check against database)
const validApiKeys = process.env.API_KEYS?.split(",") || [];
if (!validApiKeys.includes(apiKey as string)) {
return res.status(403).json({
error: "Forbidden",
message: "Invalid API key",
});
}
// Attach API key info to request
(req as any).apiKey = apiKey;
next();
};
/**
* Optional API key authentication (for public endpoints)
*/
export const optionalApiKeyAuth = (req: Request, res: Response, next: NextFunction) => {
const apiKey = req.headers["x-api-key"] || req.headers["authorization"]?.replace("Bearer ", "");
if (apiKey) {
const validApiKeys = process.env.API_KEYS?.split(",") || [];
if (validApiKeys.includes(apiKey as string)) {
(req as any).apiKey = apiKey;
(req as any).authenticated = true;
}
}
next();
};

View File

@@ -0,0 +1,53 @@
import { Request, Response, NextFunction } from "express";
interface AuditLogEntry {
timestamp: string;
requestId: string;
userId?: string;
action: string;
resource: string;
ip: string;
userAgent?: string;
success: boolean;
error?: string;
}
/**
* Audit logging middleware for sensitive operations
*/
export const auditLog = (action: string, resource: string) => {
return (req: Request, res: Response, next: NextFunction) => {
const originalSend = res.send;
const startTime = Date.now();
res.send = function (body: any) {
const duration = Date.now() - startTime;
const requestId = req.headers["x-request-id"] as string || "unknown";
const userId = (req as any).user?.id || (req as any).apiKey || "anonymous";
const ip = req.ip || req.headers["x-forwarded-for"] || req.socket.remoteAddress || "unknown";
const auditEntry: AuditLogEntry = {
timestamp: new Date().toISOString(),
requestId,
userId: userId as string,
action,
resource,
ip: ip as string,
userAgent: req.headers["user-agent"],
success: res.statusCode < 400,
error: res.statusCode >= 400 ? body : undefined,
};
// Log to audit system (in production, send to dedicated audit service)
console.log("[AUDIT]", JSON.stringify(auditEntry));
// In production, send to audit service
// auditService.log(auditEntry);
return originalSend.call(this, body);
};
next();
};
};

View File

@@ -0,0 +1,8 @@
export { apiLimiter, authLimiter, planCreationLimiter, executionLimiter } from "./rateLimit";
export { securityHeaders, requestSizeLimits, requestId } from "./security";
export { apiKeyAuth, optionalApiKeyAuth } from "./apiKeyAuth";
export { validate, sanitizeInput } from "./validation";
export { ipWhitelist, getClientIP } from "./ipWhitelist";
export { auditLog } from "./auditLog";
export { sessionManager } from "./session";

View File

@@ -0,0 +1,31 @@
import { Request, Response, NextFunction } from "express";
/**
* IP whitelist middleware for admin endpoints
*/
export const ipWhitelist = (allowedIPs: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const clientIP = req.ip || req.headers["x-forwarded-for"] || req.socket.remoteAddress;
if (!clientIP || !allowedIPs.includes(clientIP as string)) {
return res.status(403).json({
error: "Forbidden",
message: "Access denied from this IP address",
});
}
next();
};
};
/**
* Get client IP from request
*/
export const getClientIP = (req: Request): string => {
return (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() ||
req.headers["x-real-ip"] as string ||
req.ip ||
req.socket.remoteAddress ||
"unknown";
};

View File

@@ -0,0 +1,41 @@
import rateLimit from "express-rate-limit";
/**
* General API rate limiter
*/
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: "Too many requests from this IP, please try again later.",
standardHeaders: true,
legacyHeaders: false,
});
/**
* Strict rate limiter for authentication endpoints
*/
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: "Too many authentication attempts, please try again later.",
skipSuccessfulRequests: true,
});
/**
* Rate limiter for plan creation
*/
export const planCreationLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // Limit each IP to 10 plan creations per hour
message: "Too many plan creation attempts, please try again later.",
});
/**
* Rate limiter for execution endpoints
*/
export const executionLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 20, // Limit each IP to 20 executions per hour
message: "Too many execution attempts, please try again later.",
});

View File

@@ -0,0 +1,59 @@
import helmet from "helmet";
import { Request, Response, NextFunction } from "express";
/**
* Security headers middleware
*/
export const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: {
action: "deny",
},
noSniff: true,
xssFilter: true,
});
/**
* Request size limits
*/
export const requestSizeLimits = (req: Request, res: Response, next: NextFunction) => {
// Set body size limit to 10MB
if (req.headers["content-length"]) {
const contentLength = parseInt(req.headers["content-length"], 10);
if (contentLength > 10 * 1024 * 1024) {
return res.status(413).json({
error: "Request entity too large",
message: "Maximum request size is 10MB",
});
}
}
next();
};
/**
* Request ID middleware for tracking
*/
export const requestId = (req: Request, res: Response, next: NextFunction) => {
const id = req.headers["x-request-id"] || `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
req.headers["x-request-id"] = id;
res.setHeader("X-Request-ID", id);
next();
};

View File

@@ -0,0 +1,71 @@
import { Request, Response, NextFunction } from "express";
import { v4 as uuidv4 } from "uuid";
interface SessionData {
sessionId: string;
userId?: string;
createdAt: number;
lastActivity: number;
expiresAt: number;
}
const sessions: Map<string, SessionData> = new Map();
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
const MAX_SESSION_AGE = 24 * 60 * 60 * 1000; // 24 hours
/**
* Session management middleware
*/
export const sessionManager = (req: Request, res: Response, next: NextFunction) => {
const sessionId = req.headers["x-session-id"] || req.cookies?.sessionId;
if (sessionId && sessions.has(sessionId)) {
const session = sessions.get(sessionId)!;
const now = Date.now();
// Check if session expired
if (now > session.expiresAt || now - session.lastActivity > SESSION_TIMEOUT) {
sessions.delete(sessionId);
return res.status(401).json({
error: "Session expired",
message: "Please sign in again",
});
}
// Update last activity
session.lastActivity = now;
(req as any).session = session;
} else {
// Create new session
const newSession: SessionData = {
sessionId: uuidv4(),
createdAt: Date.now(),
lastActivity: Date.now(),
expiresAt: Date.now() + MAX_SESSION_AGE,
};
sessions.set(newSession.sessionId, newSession);
(req as any).session = newSession;
res.setHeader("X-Session-ID", newSession.sessionId);
}
// Cleanup expired sessions
cleanupExpiredSessions();
next();
};
/**
* Cleanup expired sessions
*/
function cleanupExpiredSessions() {
const now = Date.now();
for (const [sessionId, session] of sessions.entries()) {
if (now > session.expiresAt || now - session.lastActivity > SESSION_TIMEOUT) {
sessions.delete(sessionId);
}
}
}
// Run cleanup every 5 minutes
setInterval(cleanupExpiredSessions, 5 * 60 * 1000);

View File

@@ -0,0 +1,57 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
/**
* Request validation middleware using Zod
*/
export const validate = (schema: z.ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: "Validation failed",
errors: error.errors,
});
}
next(error);
}
};
};
/**
* Sanitize input to prevent XSS
*/
export const sanitizeInput = (req: Request, res: Response, next: NextFunction) => {
const sanitize = (obj: any): any => {
if (typeof obj === "string") {
// Remove potentially dangerous characters
return obj
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
.replace(/javascript:/gi, "")
.replace(/on\w+\s*=/gi, "");
}
if (Array.isArray(obj)) {
return obj.map(sanitize);
}
if (obj && typeof obj === "object") {
const sanitized: any = {};
for (const key in obj) {
sanitized[key] = sanitize(obj[key]);
}
return sanitized;
}
return obj;
};
if (req.body) {
req.body = sanitize(req.body);
}
if (req.query) {
req.query = sanitize(req.query);
}
next();
};

View File

@@ -0,0 +1,106 @@
import Redis from "ioredis";
/**
* Redis caching service
*/
let redis: Redis | null = null;
/**
* Initialize Redis connection
*/
export function initRedis(url?: string): Redis {
if (!redis) {
redis = new Redis(url || process.env.REDIS_URL || "redis://localhost:6379", {
maxRetriesPerRequest: 3,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
});
redis.on("error", (err) => {
console.error("Redis connection error:", err);
});
redis.on("connect", () => {
console.log("✅ Redis connected");
});
}
return redis;
}
/**
* Get Redis client
*/
export function getRedis(): Redis | null {
if (!redis && process.env.REDIS_URL) {
initRedis();
}
return redis;
}
/**
* Cache wrapper with TTL
*/
export async function cacheGet<T>(key: string): Promise<T | null> {
const client = getRedis();
if (!client) return null;
try {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error("Cache get error:", error);
return null;
}
}
export async function cacheSet<T>(key: string, value: T, ttlSeconds = 3600): Promise<void> {
const client = getRedis();
if (!client) return;
try {
await client.setex(key, ttlSeconds, JSON.stringify(value));
} catch (error) {
console.error("Cache set error:", error);
}
}
export async function cacheDelete(key: string): Promise<void> {
const client = getRedis();
if (!client) return;
try {
await client.del(key);
} catch (error) {
console.error("Cache delete error:", error);
}
}
/**
* Cache middleware for Express routes
*/
export function cacheMiddleware(ttlSeconds = 300) {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.method !== "GET") {
return next();
}
const cacheKey = `cache:${req.path}:${JSON.stringify(req.query)}`;
const cached = await cacheGet(cacheKey);
if (cached) {
return res.json(cached);
}
const originalSend = res.json;
res.json = function (body: any) {
cacheSet(cacheKey, body, ttlSeconds).catch(console.error);
return originalSend.call(this, body);
};
next();
};
}

View File

@@ -0,0 +1,62 @@
import { query } from "../db/postgres";
interface DeadLetterMessage {
messageId: string;
originalQueue: string;
payload: any;
error: string;
retryCount: number;
createdAt: string;
}
/**
* Add message to dead letter queue
*/
export async function addToDLQ(
queue: string,
payload: any,
error: string
): Promise<void> {
const messageId = `dlq-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
await query(
`INSERT INTO dead_letter_queue (message_id, queue, payload, error, retry_count, created_at)
VALUES ($1, $2, $3, $4, $5, $6)`,
[messageId, queue, JSON.stringify(payload), error, 0, new Date().toISOString()]
);
}
/**
* Get messages from DLQ for retry
*/
export async function getDLQMessages(queue: string, limit = 10): Promise<DeadLetterMessage[]> {
const result = await query<DeadLetterMessage>(
`SELECT * FROM dead_letter_queue
WHERE queue = $1 AND retry_count < 3
ORDER BY created_at ASC
LIMIT $2`,
[queue, limit]
);
return result.map((row) => ({
messageId: row.message_id,
originalQueue: row.queue,
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
error: row.error,
retryCount: row.retry_count,
createdAt: row.created_at,
}));
}
/**
* Increment retry count
*/
export async function incrementRetryCount(messageId: string): Promise<void> {
await query(
`UPDATE dead_letter_queue
SET retry_count = retry_count + 1, updated_at = CURRENT_TIMESTAMP
WHERE message_id = $1`,
[messageId]
);
}

View File

@@ -0,0 +1,103 @@
import { Request, Response, NextFunction } from "express";
import { logger } from "../logging/logger";
/**
* Error classification
*/
export enum ErrorType {
USER_ERROR = "USER_ERROR",
SYSTEM_ERROR = "SYSTEM_ERROR",
VALIDATION_ERROR = "VALIDATION_ERROR",
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR",
AUTHORIZATION_ERROR = "AUTHORIZATION_ERROR",
NOT_FOUND_ERROR = "NOT_FOUND_ERROR",
RATE_LIMIT_ERROR = "RATE_LIMIT_ERROR",
EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR",
}
/**
* Custom error class
*/
export class AppError extends Error {
constructor(
public type: ErrorType,
public statusCode: number,
message: string,
public details?: any
) {
super(message);
this.name = "AppError";
}
}
/**
* Error handling middleware
*/
export function errorHandler(
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
) {
const requestId = req.headers["x-request-id"] as string || "unknown";
// Handle known application errors
if (err instanceof AppError) {
logger.warn({
error: err,
type: err.type,
requestId,
path: req.path,
}, `Application error: ${err.message}`);
return res.status(err.statusCode).json({
error: err.type,
message: err.message,
details: err.details,
requestId,
});
}
// Handle validation errors
if (err.name === "ValidationError" || err.name === "ZodError") {
logger.warn({
error: err,
requestId,
path: req.path,
}, "Validation error");
return res.status(400).json({
error: ErrorType.VALIDATION_ERROR,
message: "Validation failed",
details: err.message,
requestId,
});
}
// Handle unknown errors
logger.error({
error: err,
requestId,
path: req.path,
stack: err.stack,
}, "Unhandled error");
res.status(500).json({
error: ErrorType.SYSTEM_ERROR,
message: "An internal server error occurred",
requestId,
...(process.env.NODE_ENV === "development" && { details: err.message }),
});
}
/**
* Async error wrapper
*/
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}

View File

@@ -0,0 +1,61 @@
/**
* Feature flags service with LaunchDarkly integration
*/
interface FeatureFlag {
key: string;
value: boolean;
defaultValue: boolean;
}
const flags: Map<string, boolean> = new Map();
/**
* Initialize feature flags
*/
export function initFeatureFlags() {
// Load from environment variables
const envFlags = {
enableRecursion: process.env.ENABLE_RECURSION === "true",
enableFlashLoans: process.env.ENABLE_FLASH_LOANS === "true",
enableSimulation: process.env.ENABLE_SIMULATION === "true",
enableWebSocket: process.env.ENABLE_WEBSOCKET === "true",
};
Object.entries(envFlags).forEach(([key, value]) => {
flags.set(key, value);
});
}
/**
* Get feature flag value
*/
export function getFeatureFlag(key: string, defaultValue = false): boolean {
return flags.get(key) ?? defaultValue;
}
/**
* Set feature flag (for testing/admin)
*/
export function setFeatureFlag(key: string, value: boolean) {
flags.set(key, value);
}
/**
* LaunchDarkly integration (optional)
*/
export class LaunchDarklyService {
private client: any;
constructor(ldClient: any) {
this.client = ldClient;
}
async getFlag(key: string, defaultValue = false): Promise<boolean> {
if (this.client) {
return await this.client.variation(key, { key: "user" }, defaultValue);
}
return defaultValue;
}
}

View File

@@ -0,0 +1,62 @@
/**
* Graceful degradation strategies
*/
export interface DegradationStrategy {
fallback: () => Promise<any>;
timeout?: number;
}
/**
* Execute with graceful degradation
*/
export async function executeWithDegradation<T>(
primary: () => Promise<T>,
strategies: DegradationStrategy[]
): Promise<T> {
try {
return await primary();
} catch (error) {
// Try fallback strategies in order
for (const strategy of strategies) {
try {
if (strategy.timeout) {
return await Promise.race([
strategy.fallback(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("Fallback timeout")), strategy.timeout)
),
]);
}
return await strategy.fallback();
} catch (fallbackError) {
// Try next strategy
continue;
}
}
throw error; // All fallbacks failed
}
}
/**
* Example: Get plan with fallback to cache
*/
export async function getPlanWithFallback(planId: string, getFromCache: () => Promise<any>) {
return executeWithDegradation(
async () => {
// Primary: Get from database
const { getPlanById } = await import("../db/plans");
return await getPlanById(planId);
},
[
{
fallback: getFromCache,
timeout: 1000,
},
{
fallback: async () => ({ planId, status: "unknown" }),
},
]
);
}

View File

@@ -0,0 +1,66 @@
/**
* HSM (Hardware Security Module) integration service
* For cryptographic operations in production
*/
export interface HSMService {
sign(data: Buffer, keyId: string): Promise<Buffer>;
verify(data: Buffer, signature: Buffer, keyId: string): Promise<boolean>;
generateKey(keyId: string): Promise<string>;
encrypt(data: Buffer, keyId: string): Promise<Buffer>;
decrypt(encrypted: Buffer, keyId: string): Promise<Buffer>;
}
/**
* Mock HSM service (for development)
* In production, integrate with actual HSM (AWS CloudHSM, Azure Dedicated HSM, etc.)
*/
export class MockHSMService implements HSMService {
private keys: Map<string, Buffer> = new Map();
async sign(data: Buffer, keyId: string): Promise<Buffer> {
// Mock implementation - in production use HSM SDK
const key = this.keys.get(keyId) || Buffer.from(keyId);
// In production: return await hsmClient.sign(data, keyId);
return Buffer.from("mock-signature");
}
async verify(data: Buffer, signature: Buffer, keyId: string): Promise<boolean> {
// Mock implementation
// In production: return await hsmClient.verify(data, signature, keyId);
return true;
}
async generateKey(keyId: string): Promise<string> {
// Mock implementation
// In production: return await hsmClient.generateKey(keyId);
const key = Buffer.from(`key-${keyId}-${Date.now()}`);
this.keys.set(keyId, key);
return keyId;
}
async encrypt(data: Buffer, keyId: string): Promise<Buffer> {
// Mock implementation
// In production: return await hsmClient.encrypt(data, keyId);
return Buffer.from(`encrypted-${data.toString()}`);
}
async decrypt(encrypted: Buffer, keyId: string): Promise<Buffer> {
// Mock implementation
// In production: return await hsmClient.decrypt(encrypted, keyId);
return Buffer.from(encrypted.toString().replace("encrypted-", ""));
}
}
/**
* Get HSM service instance
*/
export function getHSMService(): HSMService {
// In production, initialize actual HSM client
// const hsmUrl = process.env.HSM_URL;
// const hsmClient = new HSMClient(hsmUrl);
// return new HSMService(hsmClient);
return new MockHSMService();
}

View File

@@ -0,0 +1,3 @@
// Re-export cache functions
export { initRedis, getRedis, cacheGet, cacheSet, cacheDelete, cacheMiddleware } from "./cache";

View File

@@ -0,0 +1,104 @@
/**
* Secrets management service
* Supports Azure Key Vault and AWS Secrets Manager
*/
export interface SecretsService {
getSecret(name: string): Promise<string | null>;
setSecret(name: string, value: string): Promise<void>;
deleteSecret(name: string): Promise<void>;
}
/**
* Azure Key Vault implementation
*/
export class AzureKeyVaultService implements SecretsService {
private vaultUrl: string;
constructor(vaultUrl: string) {
this.vaultUrl = vaultUrl;
}
async getSecret(name: string): Promise<string | null> {
// Mock implementation - in production use @azure/keyvault-secrets
try {
// const client = new SecretClient(this.vaultUrl, credential);
// const secret = await client.getSecret(name);
// return secret.value;
return process.env[name] || null;
} catch (error) {
console.error(`Failed to get secret ${name}:`, error);
return null;
}
}
async setSecret(name: string, value: string): Promise<void> {
// Mock implementation
// const client = new SecretClient(this.vaultUrl, credential);
// await client.setSecret(name, value);
console.log(`[Secrets] Setting secret ${name} (mock)`);
}
async deleteSecret(name: string): Promise<void> {
// Mock implementation
// const client = new SecretClient(this.vaultUrl, credential);
// await client.beginDeleteSecret(name);
console.log(`[Secrets] Deleting secret ${name} (mock)`);
}
}
/**
* AWS Secrets Manager implementation
*/
export class AWSSecretsManagerService implements SecretsService {
private region: string;
constructor(region: string) {
this.region = region;
}
async getSecret(name: string): Promise<string | null> {
// Mock implementation - in production use AWS SDK
try {
// const client = new SecretsManagerClient({ region: this.region });
// const response = await client.send(new GetSecretValueCommand({ SecretId: name }));
// return response.SecretString || null;
return process.env[name] || null;
} catch (error) {
console.error(`Failed to get secret ${name}:`, error);
return null;
}
}
async setSecret(name: string, value: string): Promise<void> {
// Mock implementation
console.log(`[Secrets] Setting secret ${name} (mock)`);
}
async deleteSecret(name: string): Promise<void> {
// Mock implementation
console.log(`[Secrets] Deleting secret ${name} (mock)`);
}
}
/**
* Get secrets service instance
*/
export function getSecretsService(): SecretsService {
const vaultUrl = process.env.AZURE_KEY_VAULT_URL;
const awsRegion = process.env.AWS_SECRETS_MANAGER_REGION;
if (vaultUrl) {
return new AzureKeyVaultService(vaultUrl);
} else if (awsRegion) {
return new AWSSecretsManagerService(awsRegion);
} else {
// Fallback to environment variables
return {
getSecret: async (name: string) => process.env[name] || null,
setSecret: async () => {},
deleteSecret: async () => {},
};
}
}

View File

@@ -0,0 +1,27 @@
/**
* Timeout wrapper for async operations
*/
export function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
errorMessage = "Operation timed out"
): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(errorMessage)), timeoutMs)
),
]);
}
/**
* Create timeout configuration for different operation types
*/
export const TIMEOUTS = {
DLT_EXECUTION: 300000, // 5 minutes
BANK_API_CALL: 60000, // 1 minute
COMPLIANCE_CHECK: 30000, // 30 seconds
DATABASE_QUERY: 10000, // 10 seconds
EXTERNAL_API: 30000, // 30 seconds
};

View File

@@ -0,0 +1,68 @@
import https from "https";
import { createHash } from "crypto";
/**
* Certificate pinning for external API calls
* Prevents MITM attacks by verifying server certificates
*/
interface PinnedCertificate {
hostname: string;
fingerprints: string[]; // SHA-256 fingerprints
}
const pinnedCertificates: PinnedCertificate[] = [
// Add production certificates here
// {
// hostname: "api.bank.com",
// fingerprints: ["sha256/ABC123..."],
// },
];
/**
* Get certificate fingerprint
*/
function getCertificateFingerprint(cert: any): string {
const certBuffer = Buffer.from(cert.raw || cert.toString(), "base64");
return `sha256/${createHash("sha256").update(certBuffer).digest("base64")}`;
}
/**
* Create HTTPS agent with certificate pinning
*/
export function createPinnedAgent(hostname: string): https.Agent | null {
const pinned = pinnedCertificates.find((p) => p.hostname === hostname);
if (!pinned) {
// No pinning configured for this hostname
return null;
}
return new https.Agent({
checkServerIdentity: (servername: string, cert: any) => {
const fingerprint = getCertificateFingerprint(cert);
if (!pinned.fingerprints.includes(fingerprint)) {
throw new Error(
`Certificate pinning failed for ${servername}. Expected one of: ${pinned.fingerprints.join(", ")}, got: ${fingerprint}`
);
}
// Default certificate validation
return undefined;
},
});
}
/**
* Add certificate pin
*/
export function addCertificatePin(hostname: string, fingerprints: string[]) {
const existing = pinnedCertificates.findIndex((p) => p.hostname === hostname);
if (existing >= 0) {
pinnedCertificates[existing].fingerprints = fingerprints;
} else {
pinnedCertificates.push({ hostname, fingerprints });
}
}

View File

@@ -0,0 +1,72 @@
import { z } from "zod";
/**
* Plan validation schema
*/
export const planSchema = z.object({
creator: z.string().min(1),
steps: z.array(z.object({
type: z.enum(["borrow", "swap", "repay", "pay"]),
asset: z.string().optional(),
amount: z.number().positive(),
from: z.string().optional(),
to: z.string().optional(),
collateralRef: z.string().optional(),
beneficiary: z.object({
IBAN: z.string().optional(),
BIC: z.string().optional(),
name: z.string().optional(),
}).optional(),
})).min(1),
maxRecursion: z.number().int().min(0).max(10).optional(),
maxLTV: z.number().min(0).max(1).optional(),
signature: z.string().optional(),
});
/**
* Signature validation schema
*/
export const signatureSchema = z.object({
signature: z.string().min(1),
messageHash: z.string().min(1),
signerAddress: z.string().min(1),
});
/**
* Compliance check schema
*/
export const complianceCheckSchema = z.object({
steps: z.array(z.any()),
});
/**
* Sanitize string input
*/
export function sanitizeString(input: string): string {
return input
.replace(/[<>]/g, "") // Remove angle brackets
.replace(/javascript:/gi, "") // Remove javascript: protocol
.replace(/on\w+\s*=/gi, "") // Remove event handlers
.trim();
}
/**
* Sanitize object recursively
*/
export function sanitizeObject<T>(obj: T): T {
if (typeof obj === "string") {
return sanitizeString(obj) as T;
}
if (Array.isArray(obj)) {
return obj.map(sanitizeObject) as T;
}
if (obj && typeof obj === "object") {
const sanitized: any = {};
for (const key in obj) {
sanitized[key] = sanitizeObject(obj[key]);
}
return sanitized as T;
}
return obj;
}