// @ledgerflow/shared/index.js // Shared utilities for all Lambda functions const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { DynamoDBDocumentClient } = require("@aws-sdk/lib-dynamodb"); // ─── HTTP Response Helpers ─────────────────────────────────────────────────── const ALLOWED_ORIGINS = [ process.env.ALLOWED_ORIGIN || "*", "http://localhost:3000", ]; function getCorsHeaders(event) { const origin = event?.headers?.origin || event?.headers?.Origin || ""; const matched = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]; return { "Access-Control-Allow-Origin": matched, "Access-Control-Allow-Headers": "Content-Type,Authorization", "Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", "Content-Type": "application/json", }; } // Static fallback for responses that don't have event context const CORS_HEADERS = { "Access-Control-Allow-Origin": process.env.ALLOWED_ORIGIN || "*", "Access-Control-Allow-Headers": "Content-Type,Authorization", "Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", "Content-Type": "application/json", }; function ok(body, statusCode = 200) { return { statusCode, headers: CORS_HEADERS, body: JSON.stringify(body), }; } function created(body) { return ok(body, 201); } function noContent() { return { statusCode: 204, headers: CORS_HEADERS, body: "" }; } function badRequest(message, details = null) { return { statusCode: 400, headers: CORS_HEADERS, body: JSON.stringify({ error: "Bad Request", message, details }), }; } function unauthorized(message = "Unauthorized") { return { statusCode: 401, headers: CORS_HEADERS, body: JSON.stringify({ error: "Unauthorized", message }), }; } function forbidden(message = "Forbidden") { return { statusCode: 403, headers: CORS_HEADERS, body: JSON.stringify({ error: "Forbidden", message }), }; } function notFound(resource = "Resource") { return { statusCode: 404, headers: CORS_HEADERS, body: JSON.stringify({ error: "Not Found", message: `${resource} not found` }), }; } function conflict(message) { return { statusCode: 409, headers: CORS_HEADERS, body: JSON.stringify({ error: "Conflict", message }), }; } function serverError(message = "Internal server error", err = null) { if (err) console.error("[ERROR]", err); return { statusCode: 500, headers: CORS_HEADERS, body: JSON.stringify({ error: "Internal Server Error", message }), }; } // ─── DynamoDB Client ───────────────────────────────────────────────────────── let _docClient = null; function getDocClient() { if (!_docClient) { const base = new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" }); _docClient = DynamoDBDocumentClient.from(base, { marshallOptions: { removeUndefinedValues: true }, }); } return _docClient; } // ─── Table Names ───────────────────────────────────────────────────────────── const TABLES = { POS: process.env.POS_TABLE || "ledgerflow-pos", INVOICES: process.env.INVOICES_TABLE || "ledgerflow-invoices", EDI_TX: process.env.EDI_TX_TABLE || "ledgerflow-edi-transactions", SESSIONS: process.env.SESSIONS_TABLE || "ledgerflow-sessions", SETTINGS: process.env.SETTINGS_TABLE || "ledgerflow-settings", }; // ─── Pagination Helper ─────────────────────────────────────────────────────── function parsePagination(queryParams = {}) { const limit = Math.min(parseInt(queryParams.limit || "50", 10), 200); const cursor = queryParams.cursor || null; return { limit, cursor }; } function paginatedResponse(items, lastKey, total = null) { return { items, cursor: lastKey ? Buffer.from(JSON.stringify(lastKey)).toString("base64") : null, ...(total !== null && { total }), }; } function decodeCursor(cursor) { if (!cursor) return undefined; try { return JSON.parse(Buffer.from(cursor, "base64").toString("utf-8")); } catch { return undefined; } } // ─── ID Generation ─────────────────────────────────────────────────────────── function genId(prefix = "") { const ts = Date.now().toString(36); const rand = Math.random().toString(36).slice(2, 8); return `${prefix}${ts}${rand}`.toUpperCase(); } // ─── Input Validation ──────────────────────────────────────────────────────── function parseBody(event) { if (!event.body) return {}; try { return JSON.parse(event.body); } catch { throw new ValidationError("Invalid JSON body"); } } class ValidationError extends Error { constructor(message, details = null) { super(message); this.name = "ValidationError"; this.details = details; } } function require_fields(obj, fields) { const missing = fields.filter((f) => !obj[f] && obj[f] !== 0); if (missing.length) { throw new ValidationError(`Missing required fields: ${missing.join(", ")}`); } } // ─── Lambda Handler Wrapper ────────────────────────────────────────────────── // Catches errors, handles OPTIONS preflight, extracts auth context function handler(fn) { return async (event, context) => { const corsHeaders = getCorsHeaders(event); // CORS preflight if (event.requestContext?.http?.method === "OPTIONS" || event.httpMethod === "OPTIONS") { return { statusCode: 200, headers: corsHeaders, body: "" }; } // Attach authenticated user from authorizer context const user = event.requestContext?.authorizer?.lambda || null; try { const response = await fn(event, context, user); // Override CORS origin with the matched request origin if (response?.headers) response.headers["Access-Control-Allow-Origin"] = corsHeaders["Access-Control-Allow-Origin"]; return response; } catch (err) { if (err instanceof ValidationError) { return badRequest(err.message, err.details); } console.error("[UNHANDLED]", err); return serverError("An unexpected error occurred", err); } }; } module.exports = { // Response ok, created, noContent, badRequest, unauthorized, forbidden, notFound, conflict, serverError, CORS_HEADERS, // DynamoDB getDocClient, TABLES, // Pagination parsePagination, paginatedResponse, decodeCursor, // Util genId, parseBody, require_fields, handler, ValidationError, };