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:
49
orchestrator/src/api/execution.ts
Normal file
49
orchestrator/src/api/execution.ts
Normal 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 });
|
||||
});
|
||||
|
||||
38
orchestrator/src/api/swagger.ts
Normal file
38
orchestrator/src/api/swagger.ts
Normal 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));
|
||||
}
|
||||
|
||||
22
orchestrator/src/api/version.ts
Normal file
22
orchestrator/src/api/version.ts
Normal 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;
|
||||
}
|
||||
|
||||
78
orchestrator/src/api/webhooks.ts
Normal file
78
orchestrator/src/api/webhooks.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
|
||||
57
orchestrator/src/config/env.ts
Normal file
57
orchestrator/src/config/env.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
47
orchestrator/src/db/migrations/001_initial_schema.ts
Normal file
47
orchestrator/src/db/migrations/001_initial_schema.ts
Normal 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");
|
||||
}
|
||||
|
||||
15
orchestrator/src/db/migrations/index.ts
Normal file
15
orchestrator/src/db/migrations/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
94
orchestrator/src/db/postgres.ts
Normal file
94
orchestrator/src/db/postgres.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
139
orchestrator/src/db/schema.sql
Normal file
139
orchestrator/src/db/schema.sql
Normal 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();
|
||||
|
||||
78
orchestrator/src/health/health.ts
Normal file
78
orchestrator/src/health/health.ts
Normal 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
139
orchestrator/src/index.ts
Normal 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();
|
||||
|
||||
74
orchestrator/src/logging/logger.ts
Normal file
74
orchestrator/src/logging/logger.ts
Normal 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));
|
||||
}
|
||||
|
||||
79
orchestrator/src/metrics/prometheus.ts
Normal file
79
orchestrator/src/metrics/prometheus.ts
Normal 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();
|
||||
}
|
||||
|
||||
44
orchestrator/src/middleware/apiKeyAuth.ts
Normal file
44
orchestrator/src/middleware/apiKeyAuth.ts
Normal 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();
|
||||
};
|
||||
|
||||
53
orchestrator/src/middleware/auditLog.ts
Normal file
53
orchestrator/src/middleware/auditLog.ts
Normal 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();
|
||||
};
|
||||
};
|
||||
|
||||
8
orchestrator/src/middleware/index.ts
Normal file
8
orchestrator/src/middleware/index.ts
Normal 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";
|
||||
|
||||
31
orchestrator/src/middleware/ipWhitelist.ts
Normal file
31
orchestrator/src/middleware/ipWhitelist.ts
Normal 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";
|
||||
};
|
||||
|
||||
41
orchestrator/src/middleware/rateLimit.ts
Normal file
41
orchestrator/src/middleware/rateLimit.ts
Normal 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.",
|
||||
});
|
||||
|
||||
59
orchestrator/src/middleware/security.ts
Normal file
59
orchestrator/src/middleware/security.ts
Normal 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();
|
||||
};
|
||||
|
||||
71
orchestrator/src/middleware/session.ts
Normal file
71
orchestrator/src/middleware/session.ts
Normal 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);
|
||||
|
||||
57
orchestrator/src/middleware/validation.ts
Normal file
57
orchestrator/src/middleware/validation.ts
Normal 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();
|
||||
};
|
||||
|
||||
106
orchestrator/src/services/cache.ts
Normal file
106
orchestrator/src/services/cache.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
|
||||
62
orchestrator/src/services/deadLetterQueue.ts
Normal file
62
orchestrator/src/services/deadLetterQueue.ts
Normal 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]
|
||||
);
|
||||
}
|
||||
|
||||
103
orchestrator/src/services/errorHandler.ts
Normal file
103
orchestrator/src/services/errorHandler.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
|
||||
61
orchestrator/src/services/featureFlags.ts
Normal file
61
orchestrator/src/services/featureFlags.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
62
orchestrator/src/services/gracefulDegradation.ts
Normal file
62
orchestrator/src/services/gracefulDegradation.ts
Normal 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" }),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
66
orchestrator/src/services/hsm.ts
Normal file
66
orchestrator/src/services/hsm.ts
Normal 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();
|
||||
}
|
||||
|
||||
3
orchestrator/src/services/redis.ts
Normal file
3
orchestrator/src/services/redis.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Re-export cache functions
|
||||
export { initRedis, getRedis, cacheGet, cacheSet, cacheDelete, cacheMiddleware } from "./cache";
|
||||
|
||||
104
orchestrator/src/services/secrets.ts
Normal file
104
orchestrator/src/services/secrets.ts
Normal 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 () => {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
27
orchestrator/src/services/timeout.ts
Normal file
27
orchestrator/src/services/timeout.ts
Normal 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
|
||||
};
|
||||
|
||||
68
orchestrator/src/utils/certificatePinning.ts
Normal file
68
orchestrator/src/utils/certificatePinning.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
||||
72
orchestrator/src/utils/inputValidation.ts
Normal file
72
orchestrator/src/utils/inputValidation.ts
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user