All files / lib response.ts

100% Statements 30/30
100% Branches 27/27
100% Functions 5/5
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169                                                                  1176x                                                 164x                               1036x                             769x         769x 1x                 768x 542x                 226x 1x       225x 16x         201x               16x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x   1x      
// Standard API response helpers
import type { Context } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import { AppError, ConfigurationError } from "./errors";
import { WhatsAppApiError, WhatsAppConfigError } from "./whatsapp/client";
 
export interface ApiResponse<T = unknown> {
	success: boolean;
	data?: T;
	error?: {
		code: string;
		message: string;
	};
	meta?: {
		total?: number;
		page?: number;
		limit?: number;
		offset?: number;
		totalPages?: number;
		hasNext?: boolean;
		hasPrev?: boolean;
	};
}
 
/**
 * Send a success response
 */
export function success<T>(
	c: Context,
	data: T,
	status: ContentfulStatusCode = 200,
	meta?: ApiResponse["meta"],
) {
	return c.json<ApiResponse<T>>(
		{
			success: true,
			data,
			...(meta && { meta }),
		},
		status,
	);
}
 
/**
 * Send a success response with pagination
 */
export function successWithPagination<T>(
	c: Context,
	data: T[],
	meta: {
		total: number;
		page: number;
		limit: number;
		totalPages: number;
		hasNext: boolean;
		hasPrev: boolean;
	},
) {
	return c.json<ApiResponse<T[]>>({
		success: true,
		data,
		meta,
	});
}
 
/**
 * Send an error response
 */
export function error(
	c: Context,
	code: string,
	message: string,
	statusCode: ContentfulStatusCode,
) {
	return c.json<ApiResponse>(
		{
			success: false,
			error: { code, message },
		},
		statusCode,
	);
}
 
/**
 * Handle errors consistently
 * Note: Internal error messages are not exposed to clients for security
 */
export function handleError(c: Context, err: unknown) {
	// Log the full error for debugging (server-side only)
	console.error("API Error:", err);
 
	// Configuration errors carry operator-only diagnostics (env var names,
	// deployment commands). Scrub before sending to client; the full message
	// is already logged above for debugging.
	if (err instanceof ConfigurationError) {
		return error(
			c,
			"CONFIGURATION_ERROR",
			"Service temporarily unavailable. Please contact support.",
			503,
		);
	}
 
	// Handle known application errors - these have safe, user-facing messages
	if (err instanceof AppError) {
		return error(
			c,
			err.code,
			err.message,
			err.statusCode as ContentfulStatusCode,
		);
	}
 
	// Handle WhatsApp config errors — service not configured
	if (err instanceof WhatsAppConfigError) {
		return error(c, "CONFIG_ERROR", "WhatsApp not configured", 500);
	}
 
	// Handle WhatsApp API errors — map to safe user-facing messages
	if (err instanceof WhatsAppApiError) {
		return error(c, "WHATSAPP_API_ERROR", mapWhatsAppError(err), 400);
	}
 
	// For unknown errors, don't expose internal details to clients
	// The actual error is logged above for debugging
	return error(c, "INTERNAL_ERROR", "An unexpected error occurred", 500);
}
 
/**
 * Map WhatsApp API error codes to safe user-facing messages.
 * Raw Meta error messages may contain internal identifiers and debug codes.
 */
function mapWhatsAppError(err: WhatsAppApiError): string {
	switch (err.code) {
		case 100:
			return "Invalid request parameter. Please check the phone number or message content.";
		case 131026:
			return "Message could not be delivered. The recipient may not have WhatsApp.";
		case 131047:
			return "Message failed to send. Please try again later.";
		case 131048:
			return "This message was flagged as spam. Please review the content.";
		case 131049:
			return "This post has been removed for policy violations.";
		case 131051:
			return "Message type is not supported.";
		case 130429:
			return "Rate limit exceeded. Please wait and try again.";
		case 131021:
			return "The recipient cannot receive messages at this time.";
		case 132000:
			return "The template parameters are missing or invalid.";
		case 132001:
			return "Template not found or not approved.";
		case 132005:
			return "Template hydration failed. Check the parameter values.";
		case 132012:
			return "Template parameter format is incorrect.";
		case 133010:
			return "Phone number is not registered on WhatsApp.";
		case 133004:
			return "Server is temporarily unavailable. Please try again.";
		case 368:
			return "Your account has been temporarily restricted. Contact support.";
		default:
			return "WhatsApp message could not be sent. Please try again.";
	}
}