@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
242 lines • 8.46 kB
JavaScript
/**
* @module runtime/ingress
* @description Ingress component for receiving and normalizing HTTP requests
*/
import { randomUUID } from 'crypto';
/**
* Ingress component implementation
* Receives external HTTP requests, authenticates, normalizes, and publishes to routing fabric
*/
export class Ingress {
config;
queueFabric;
constructor(config, queueFabric) {
this.config = config;
this.queueFabric = queueFabric;
}
/**
* Handle an incoming HTTP request
* Main entry point for all external requests
*/
async handleRequest(rawRequest) {
// Authenticate the request
const authResult = await this.authenticate(rawRequest);
if (this.config.requireAuth && !authResult.authenticated) {
throw new Error(`Authentication failed: ${authResult.error}`);
}
// Read request body
const body = await this.readBody(rawRequest);
// Normalize headers
const headers = this.normalizeHeaders(rawRequest.headers);
// Extract metadata from request
const metadata = this.extractMetadata(rawRequest, headers);
// Assign request ID
const requestId = this.assignRequestId(metadata);
// Add request ID to headers
headers['x-request-id'] = requestId;
// Create request descriptor
const descriptor = {
requestId,
path: metadata.path,
method: rawRequest.method || 'GET',
headers,
body,
versionPreference: metadata.versionPreference,
priority: metadata.priority,
flags: metadata.flags,
authContext: authResult.authenticated ? authResult : undefined,
timestamp: metadata.timestamp,
};
// Publish to routing fabric
await this.publishToRoutingFabric(descriptor);
}
/**
* Authenticate a request based on configured auth method
*/
async authenticate(request) {
const headers = request.headers;
switch (this.config.authMethod) {
case 'jwt':
return this.authenticateJWT(headers);
case 'api-key':
return this.authenticateApiKey(headers);
case 'oauth':
return this.authenticateOAuth(headers);
case 'none':
return { authenticated: true };
default:
return {
authenticated: false,
error: `Unknown auth method: ${this.config.authMethod}`,
};
}
}
/**
* Authenticate using JWT
*/
authenticateJWT(headers) {
const authHeader = headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return Promise.resolve({
authenticated: false,
error: 'Missing or invalid Authorization header',
});
}
const token = authHeader.substring(7);
if (!this.config.jwtSecret) {
return Promise.resolve({
authenticated: false,
error: 'JWT secret not configured',
});
}
try {
// Simple JWT validation (in production, use a proper JWT library)
// This is a minimal implementation for the spec
const parts = token.split('.');
if (parts.length !== 3) {
return Promise.resolve({
authenticated: false,
error: 'Invalid JWT format',
});
}
// Decode payload (without verification for now - would use jsonwebtoken in production)
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
return Promise.resolve({
authenticated: true,
clientId: payload.sub ?? payload.userId,
roles: payload.roles ?? [],
});
}
catch (error) {
return Promise.resolve({
authenticated: false,
error: `JWT validation failed: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
/**
* Authenticate using API key
*/
authenticateApiKey(headers) {
const authHeader = headers['authorization'];
const apiKeyHeader = headers['x-api-key'];
const apiKey = apiKeyHeader ?? (typeof authHeader === 'string' ? authHeader.replace('ApiKey ', '') : undefined);
if (!apiKey) {
return Promise.resolve({
authenticated: false,
error: 'Missing API key',
});
}
if (!this.config.apiKeys || !this.config.apiKeys.has(apiKey)) {
return Promise.resolve({
authenticated: false,
error: 'Invalid API key',
});
}
return Promise.resolve({
authenticated: true,
clientId: apiKey,
roles: ['api-user'],
});
}
/**
* Authenticate using OAuth
*/
authenticateOAuth(headers) {
const authHeader = headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return Promise.resolve({
authenticated: false,
error: 'Missing or invalid Authorization header',
});
}
// OAuth validation would integrate with OAuth provider
// This is a placeholder implementation
return Promise.resolve({
authenticated: true,
clientId: 'oauth-user',
roles: ['user'],
});
}
/**
* Normalize HTTP headers to consistent format
*/
normalizeHeaders(headers) {
const normalized = {
'x-request-id': '', // Will be set later
};
// Normalize header names to lowercase
for (const [key, value] of Object.entries(headers)) {
const normalizedKey = key.toLowerCase();
normalized[normalizedKey] = value;
}
// Ensure x-forwarded-for is set
if (!normalized['x-forwarded-for']) {
normalized['x-forwarded-for'] = 'unknown';
}
return normalized;
}
/**
* Assign a unique request ID with embedded metadata
*/
assignRequestId(_metadata) {
const prefix = this.config.requestIdPrefix || 'req';
const uuid = randomUUID();
// Format: prefix-uuid-timestamp
// Metadata is embedded in the descriptor, not the ID itself
return `${prefix}-${uuid}`;
}
/**
* Publish request descriptor to routing fabric
*/
async publishToRoutingFabric(descriptor) {
await this.queueFabric.publish(this.config.routingTopic, descriptor);
}
/**
* Read request body from IncomingMessage
*/
async readBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on('data', (chunk) => {
chunks.push(chunk);
});
request.on('end', () => {
resolve(Buffer.concat(chunks));
});
request.on('error', (error) => {
reject(error);
});
});
}
/**
* Extract metadata from request
*/
extractMetadata(request, headers) {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
// Extract version preference from header or query
const versionPreference = headers['x-version'] ||
url.searchParams.get('version') ||
undefined;
// Extract priority from header (default to 5)
const priorityHeader = headers['x-priority'];
const priority = priorityHeader ? parseInt(priorityHeader, 10) : 5;
// Extract debug flags from header
const flagsHeader = headers['x-flags'];
const flags = flagsHeader ? flagsHeader.split(',').map((f) => f.trim()) : [];
return {
path: url.pathname,
versionPreference,
priority: isNaN(priority) ? 5 : Math.max(0, Math.min(10, priority)),
flags,
timestamp: Date.now(),
};
}
}
/**
* Create an Ingress instance
*/
export function createIngress(config, queueFabric) {
return new Ingress(config, queueFabric);
}
//# sourceMappingURL=ingress.js.map