577 lines
19 KiB
TypeScript
577 lines
19 KiB
TypeScript
|
/**
|
||
|
* Namecheap API Client - Domains Module
|
||
|
*/
|
||
|
import { HttpClient } from './http-client.js';
|
||
|
import type {
|
||
|
IDomainsGetListParams,
|
||
|
IDomainsGetListResponse,
|
||
|
IDomainInfo,
|
||
|
IDomainCheckResponse,
|
||
|
IDomainCheckResult,
|
||
|
IDomainAvailability,
|
||
|
IDomainCreateParams,
|
||
|
IDomainCreateResponse,
|
||
|
IDomainGetInfoResponse,
|
||
|
IDomainInfoResult,
|
||
|
IDomainContacts,
|
||
|
IDomainGetContactsResponse,
|
||
|
IDomainSetContactsParams,
|
||
|
IDomainRenewResponse,
|
||
|
IDomainRenewResult,
|
||
|
IRegistrarLockResponse,
|
||
|
IDomainTldListResponse,
|
||
|
IContactInfo
|
||
|
} from './types.js';
|
||
|
|
||
|
export class Domains {
|
||
|
private client: HttpClient;
|
||
|
|
||
|
/**
|
||
|
* Create a new Domains API handler
|
||
|
* @param client HTTP client instance
|
||
|
*/
|
||
|
constructor(client: HttpClient) {
|
||
|
this.client = client;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a list of domains in the Namecheap account
|
||
|
* @param params Optional parameters for filtering and pagination
|
||
|
* @returns Array of domain information objects
|
||
|
*/
|
||
|
async getList(params: IDomainsGetListParams = {}): Promise<{
|
||
|
domains: IDomainInfo[];
|
||
|
paging: {
|
||
|
totalItems: number;
|
||
|
currentPage: number;
|
||
|
pageSize: number;
|
||
|
};
|
||
|
}> {
|
||
|
// Convert parameters to the format expected by the API
|
||
|
const requestParams: Record<string, string | number | boolean> = {};
|
||
|
|
||
|
// Add optional parameters if provided
|
||
|
if (params.Page !== undefined) {
|
||
|
requestParams.Page = params.Page;
|
||
|
}
|
||
|
|
||
|
if (params.PageSize !== undefined) {
|
||
|
requestParams.PageSize = params.PageSize;
|
||
|
}
|
||
|
|
||
|
if (params.SortBy !== undefined) {
|
||
|
requestParams.SortBy = params.SortBy;
|
||
|
}
|
||
|
|
||
|
if (params.ListType !== undefined) {
|
||
|
requestParams.ListType = params.ListType;
|
||
|
}
|
||
|
|
||
|
if (params.SearchTerm !== undefined) {
|
||
|
requestParams.SearchTerm = params.SearchTerm;
|
||
|
}
|
||
|
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainsGetListResponse>(
|
||
|
'namecheap.domains.getList',
|
||
|
requestParams
|
||
|
);
|
||
|
|
||
|
// Extract domain information from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const domainListResult = commandResponse.DomainGetListResult[0];
|
||
|
const paging = commandResponse.Paging[0];
|
||
|
|
||
|
// Convert the parsed XML structure to a more usable format
|
||
|
const domains = (domainListResult.Domain || []).map(domain => {
|
||
|
const domainInfo: IDomainInfo = {
|
||
|
...domain.$,
|
||
|
// Convert string boolean values to actual booleans
|
||
|
IsExpired: String(domain.$.IsExpired).toLowerCase() === 'true',
|
||
|
IsLocked: String(domain.$.IsLocked).toLowerCase() === 'true',
|
||
|
AutoRenew: String(domain.$.AutoRenew).toLowerCase() === 'true',
|
||
|
IsPremium: String(domain.$.IsPremium).toLowerCase() === 'true',
|
||
|
IsOurDNS: String(domain.$.IsOurDNS).toLowerCase() === 'true',
|
||
|
// Join nameservers array with commas if it exists
|
||
|
Nameservers: Array.isArray(domain.Nameservers) && domain.Nameservers.length > 0 ?
|
||
|
domain.Nameservers[0] : ''
|
||
|
};
|
||
|
|
||
|
return domainInfo;
|
||
|
});
|
||
|
|
||
|
// Convert paging information
|
||
|
const pagingInfo = paging ? {
|
||
|
totalItems: parseInt(paging.TotalItems[0], 10),
|
||
|
currentPage: parseInt(paging.CurrentPage[0], 10),
|
||
|
pageSize: parseInt(paging.PageSize[0], 10)
|
||
|
} : {
|
||
|
totalItems: domains.length,
|
||
|
currentPage: 1,
|
||
|
pageSize: domains.length
|
||
|
};
|
||
|
|
||
|
return {
|
||
|
domains,
|
||
|
paging: pagingInfo
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if domains are available for registration
|
||
|
* @param domainNames Domain name(s) to check (single string or array of strings)
|
||
|
* @returns Domain availability information
|
||
|
*/
|
||
|
async check(domainNames: string | string[]): Promise<IDomainAvailability[]> {
|
||
|
// Convert single domain to array if needed
|
||
|
const domains = Array.isArray(domainNames) ? domainNames : [domainNames];
|
||
|
|
||
|
// Join domains with comma for the API request
|
||
|
const domainList = domains.join(',');
|
||
|
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainCheckResponse>(
|
||
|
'namecheap.domains.check',
|
||
|
{ DomainList: domainList }
|
||
|
);
|
||
|
|
||
|
// Parse the response to determine availability
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const results: IDomainCheckResult[] = Array.isArray(commandResponse.DomainCheckResult)
|
||
|
? commandResponse.DomainCheckResult
|
||
|
: [commandResponse.DomainCheckResult];
|
||
|
|
||
|
// Convert the results to a more usable format
|
||
|
return results.map(result => ({
|
||
|
domain: result.$.Domain,
|
||
|
available: String(result.$.Available).toLowerCase() === 'true',
|
||
|
errorNo: parseInt(result.$.ErrorNo || '0', 10),
|
||
|
description: result.$.Description || '',
|
||
|
isPremium: String(result.$.IsPremiumName).toLowerCase() === 'true',
|
||
|
premiumRegistrationPrice: parseFloat(result.$.PremiumRegistrationPrice || '0'),
|
||
|
premiumRenewalPrice: parseFloat(result.$.PremiumRenewalPrice || '0'),
|
||
|
premiumRestorePrice: parseFloat(result.$.PremiumRestorePrice || '0'),
|
||
|
premiumTransferPrice: parseFloat(result.$.PremiumTransferPrice || '0'),
|
||
|
icannFee: parseFloat(result.$.IcannFee || '0'),
|
||
|
eapFee: parseFloat(result.$.EapFee || '0')
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get detailed information about a specific domain
|
||
|
* @param domainName Domain name to get information for
|
||
|
* @param hostName Optional host name for hosted domains
|
||
|
* @returns Detailed domain information
|
||
|
*/
|
||
|
async getInfo(domainName: string, hostName?: string): Promise<IDomainInfoResult> {
|
||
|
const params: Record<string, string> = { DomainName: domainName };
|
||
|
|
||
|
if (hostName) {
|
||
|
params.HostName = hostName;
|
||
|
}
|
||
|
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainGetInfoResponse>(
|
||
|
'namecheap.domains.getInfo',
|
||
|
params
|
||
|
);
|
||
|
|
||
|
// Extract domain information from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const domainInfo = commandResponse.DomainGetInfoResult[0];
|
||
|
|
||
|
// Convert string boolean values to actual booleans
|
||
|
const result: IDomainInfoResult = {
|
||
|
status: domainInfo.$.Status,
|
||
|
id: parseInt(domainInfo.$.ID, 10),
|
||
|
domainName: domainInfo.$.DomainName,
|
||
|
ownerName: domainInfo.$.OwnerName,
|
||
|
isOwner: String(domainInfo.$.IsOwner).toLowerCase() === 'true',
|
||
|
isPremium: String(domainInfo.$.IsPremium).toLowerCase() === 'true',
|
||
|
createdDate: domainInfo.DomainDetails?.[0]?.CreatedDate?.[0] || '',
|
||
|
expiredDate: domainInfo.DomainDetails?.[0]?.ExpiredDate?.[0] || '',
|
||
|
whoisGuard: {
|
||
|
enabled: domainInfo.Whoisguard?.[0]?.$.Enabled?.toLowerCase() === 'true' || false,
|
||
|
id: domainInfo.Whoisguard?.[0]?.ID?.[0] ? parseInt(domainInfo.Whoisguard[0].ID[0], 10) : 0,
|
||
|
expiredDate: domainInfo.Whoisguard?.[0]?.ExpiredDate?.[0] || ''
|
||
|
},
|
||
|
dnsProvider: domainInfo.DnsDetails?.[0]?.$.ProviderType || '',
|
||
|
modificationRights: {
|
||
|
all: domainInfo.Modificationrights?.[0]?.$.All?.toLowerCase() === 'true' || false
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get contact information for a domain
|
||
|
* @param domainName Domain name to get contacts for
|
||
|
* @returns Domain contact information
|
||
|
* @throws Error if the domain name is invalid or the API request fails
|
||
|
*/
|
||
|
async getContacts(domainName: string): Promise<IDomainContacts> {
|
||
|
if (!domainName) {
|
||
|
throw new Error('Domain name is required');
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainGetContactsResponse>(
|
||
|
'namecheap.domains.getContacts',
|
||
|
{ DomainName: domainName }
|
||
|
);
|
||
|
|
||
|
// Check for API errors
|
||
|
if (response.ApiResponse.$.Status !== 'OK') {
|
||
|
const errors = response.ApiResponse.Errors;
|
||
|
if (Array.isArray(errors) && errors.length > 0) {
|
||
|
const error = errors[0] as { Error?: string[] };
|
||
|
if (error.Error && error.Error.length > 0) {
|
||
|
throw new Error(`API Error: ${error.Error.join(', ')}`);
|
||
|
}
|
||
|
}
|
||
|
throw new Error('Failed to get domain contacts');
|
||
|
}
|
||
|
|
||
|
// Extract contact information from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const contacts = commandResponse.DomainContactsResult[0];
|
||
|
|
||
|
// Convert the parsed XML structure to a more usable format
|
||
|
return {
|
||
|
registrant: this.parseContact(contacts.Registrant?.[0]),
|
||
|
tech: this.parseContact(contacts.Tech?.[0]),
|
||
|
admin: this.parseContact(contacts.Admin?.[0]),
|
||
|
auxBilling: this.parseContact(contacts.AuxBilling?.[0])
|
||
|
};
|
||
|
} catch (error) {
|
||
|
if (error instanceof Error) {
|
||
|
throw error;
|
||
|
}
|
||
|
throw new Error(`Failed to get domain contacts: ${String(error)}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set contact information for a domain
|
||
|
* @param domainName Domain name to set contacts for
|
||
|
* @param contacts Contact information to set
|
||
|
* @returns Success status
|
||
|
* @throws Error if the domain name is invalid, contacts are missing, or the API request fails
|
||
|
*/
|
||
|
async setContacts(domainName: string, contacts: IDomainSetContactsParams): Promise<boolean> {
|
||
|
// Validate inputs
|
||
|
if (!domainName) {
|
||
|
throw new Error('Domain name is required');
|
||
|
}
|
||
|
|
||
|
if (!contacts || Object.keys(contacts).length === 0) {
|
||
|
throw new Error('Contact information is required');
|
||
|
}
|
||
|
|
||
|
// Ensure at least one contact type is provided
|
||
|
if (!contacts.registrant && !contacts.tech && !contacts.admin && !contacts.auxBilling) {
|
||
|
throw new Error('At least one contact type (registrant, tech, admin, or auxBilling) is required');
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// Prepare the request parameters
|
||
|
const params: Record<string, string> = {
|
||
|
DomainName: domainName,
|
||
|
...this.flattenContacts(contacts)
|
||
|
};
|
||
|
|
||
|
// Make the API request
|
||
|
const response = await this.client.request(
|
||
|
'namecheap.domains.setContacts',
|
||
|
params
|
||
|
);
|
||
|
|
||
|
// Check for API errors
|
||
|
if (response.ApiResponse.$.Status !== 'OK') {
|
||
|
const errors = response.ApiResponse.Errors;
|
||
|
if (Array.isArray(errors) && errors.length > 0) {
|
||
|
const error = errors[0] as { Error?: string[] };
|
||
|
if (error.Error && error.Error.length > 0) {
|
||
|
throw new Error(`API Error: ${error.Error.join(', ')}`);
|
||
|
}
|
||
|
}
|
||
|
throw new Error('Failed to set domain contacts');
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
} catch (error) {
|
||
|
if (error instanceof Error) {
|
||
|
throw error;
|
||
|
}
|
||
|
throw new Error(`Failed to set domain contacts: ${String(error)}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Register a new domain
|
||
|
* @param params Domain registration parameters
|
||
|
* @returns Registration result
|
||
|
*/
|
||
|
async create(params: IDomainCreateParams): Promise<{
|
||
|
domain: string;
|
||
|
registered: boolean;
|
||
|
chargedAmount: number;
|
||
|
transactionId: number;
|
||
|
orderId: number;
|
||
|
}> {
|
||
|
// Prepare the request parameters
|
||
|
const requestParams: Record<string, string | number | boolean> = {
|
||
|
DomainName: params.domainName,
|
||
|
Years: params.years,
|
||
|
...this.flattenContacts(params.contacts)
|
||
|
};
|
||
|
|
||
|
// Add nameservers if provided
|
||
|
if (params.nameservers && params.nameservers.length > 0) {
|
||
|
requestParams.Nameservers = params.nameservers.join(',');
|
||
|
}
|
||
|
|
||
|
// Add additional parameters
|
||
|
if (params.addFreeWhoisguard !== undefined) {
|
||
|
requestParams.AddFreeWhoisguard = params.addFreeWhoisguard;
|
||
|
}
|
||
|
|
||
|
if (params.whoisguardPrivacy !== undefined) {
|
||
|
requestParams.WGEnabled = params.whoisguardPrivacy;
|
||
|
}
|
||
|
|
||
|
if (params.premiumPrice !== undefined) {
|
||
|
requestParams.PremiumPrice = params.premiumPrice;
|
||
|
}
|
||
|
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainCreateResponse>(
|
||
|
'namecheap.domains.create',
|
||
|
requestParams
|
||
|
);
|
||
|
|
||
|
// Extract registration information from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const domainCreateResult = commandResponse.DomainCreateResult[0];
|
||
|
|
||
|
return {
|
||
|
domain: domainCreateResult.$.Domain,
|
||
|
registered: String(domainCreateResult.$.Registered).toLowerCase() === 'true',
|
||
|
chargedAmount: parseFloat(domainCreateResult.$.ChargedAmount),
|
||
|
transactionId: parseInt(domainCreateResult.$.TransactionID, 10),
|
||
|
orderId: parseInt(domainCreateResult.$.OrderID, 10)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Renew a domain registration
|
||
|
* @param domainName Domain name to renew
|
||
|
* @param years Number of years to renew for
|
||
|
* @param premiumPrice Optional premium price for premium domains
|
||
|
* @returns Renewal result
|
||
|
*/
|
||
|
async renew(domainName: string, years: number, premiumPrice?: number): Promise<IDomainRenewResult> {
|
||
|
// Prepare the request parameters
|
||
|
const params: Record<string, string | number | boolean> = {
|
||
|
DomainName: domainName,
|
||
|
Years: years
|
||
|
};
|
||
|
|
||
|
if (premiumPrice !== undefined) {
|
||
|
params.PremiumPrice = premiumPrice;
|
||
|
}
|
||
|
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainRenewResponse>(
|
||
|
'namecheap.domains.renew',
|
||
|
params
|
||
|
);
|
||
|
|
||
|
// Extract renewal information from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const renewResult = commandResponse.DomainRenewResult[0].$;
|
||
|
|
||
|
return {
|
||
|
domainName: renewResult.DomainName,
|
||
|
domainId: parseInt(renewResult.DomainID, 10),
|
||
|
renewed: String(renewResult.Renewed).toLowerCase() === 'true',
|
||
|
chargedAmount: parseFloat(renewResult.ChargedAmount),
|
||
|
transactionId: parseInt(renewResult.TransactionID, 10),
|
||
|
orderId: parseInt(renewResult.OrderID, 10),
|
||
|
expireDate: renewResult.DomainDetails?.ExpiredDate || ''
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reactivate an expired domain
|
||
|
* @param domainName Domain name to reactivate
|
||
|
* @returns Reactivation result
|
||
|
*/
|
||
|
async reactivate(domainName: string): Promise<{
|
||
|
domain: string;
|
||
|
reactivated: boolean;
|
||
|
chargedAmount: number;
|
||
|
orderId: number;
|
||
|
transactionId: number;
|
||
|
}> {
|
||
|
// Make the API request
|
||
|
const response = await this.client.request(
|
||
|
'namecheap.domains.reactivate',
|
||
|
{ DomainName: domainName }
|
||
|
);
|
||
|
|
||
|
// Extract reactivation information from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const reactivateResult = commandResponse.DomainReactivateResult[0].$;
|
||
|
|
||
|
return {
|
||
|
domain: reactivateResult.Domain,
|
||
|
reactivated: String(reactivateResult.IsSuccess).toLowerCase() === 'true',
|
||
|
chargedAmount: parseFloat(reactivateResult.ChargedAmount),
|
||
|
orderId: parseInt(reactivateResult.OrderID, 10),
|
||
|
transactionId: parseInt(reactivateResult.TransactionID, 10)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the registrar lock status for a domain
|
||
|
* @param domainName Domain name to check
|
||
|
* @returns Lock status
|
||
|
*/
|
||
|
async getRegistrarLock(domainName: string): Promise<boolean> {
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IRegistrarLockResponse>(
|
||
|
'namecheap.domains.getRegistrarLock',
|
||
|
{ DomainName: domainName }
|
||
|
);
|
||
|
|
||
|
// Extract lock status from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const lockStatus = commandResponse.DomainGetRegistrarLockResult[0];
|
||
|
|
||
|
return String(lockStatus.$.RegistrarLockStatus).toLowerCase() === 'true';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the registrar lock status for a domain
|
||
|
* @param domainName Domain name to update
|
||
|
* @param lockStatus New lock status (true to lock, false to unlock)
|
||
|
* @returns Success status
|
||
|
*/
|
||
|
async setRegistrarLock(domainName: string, lockStatus: boolean): Promise<boolean> {
|
||
|
// Make the API request
|
||
|
const response = await this.client.request(
|
||
|
'namecheap.domains.setRegistrarLock',
|
||
|
{
|
||
|
DomainName: domainName,
|
||
|
LockAction: lockStatus ? 'LOCK' : 'UNLOCK'
|
||
|
}
|
||
|
);
|
||
|
|
||
|
// Check if the operation was successful
|
||
|
return response.ApiResponse.$.Status === 'OK';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a list of available TLDs
|
||
|
* @returns List of TLDs
|
||
|
*/
|
||
|
async getTldList(): Promise<string[]> {
|
||
|
// Make the API request
|
||
|
const response = await this.client.request<IDomainTldListResponse>(
|
||
|
'namecheap.domains.getTldList'
|
||
|
);
|
||
|
|
||
|
// Extract TLD list from the response
|
||
|
const commandResponse = response.ApiResponse.CommandResponse[0];
|
||
|
const tlds = commandResponse.Tlds[0].Tld || [];
|
||
|
|
||
|
return tlds.map(tld => tld.$.Name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper method to parse contact information
|
||
|
* @param contactData Contact data from API response
|
||
|
* @returns Parsed contact information
|
||
|
*/
|
||
|
private parseContact(contactData: any): IContactInfo {
|
||
|
if (!contactData) {
|
||
|
return {};
|
||
|
}
|
||
|
|
||
|
const contact: IContactInfo = {};
|
||
|
|
||
|
// Map all contact fields
|
||
|
Object.keys(contactData).forEach(key => {
|
||
|
if (Array.isArray(contactData[key]) && contactData[key].length > 0) {
|
||
|
// Use type assertion to handle dynamic property assignment
|
||
|
(contact as any)[key] = contactData[key][0];
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return contact;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper method to flatten contact information for API requests
|
||
|
* @param contacts Contact information object
|
||
|
* @returns Flattened contact parameters
|
||
|
*/
|
||
|
private flattenContacts(contacts: IDomainSetContactsParams): Record<string, string> {
|
||
|
const params: Record<string, string> = {};
|
||
|
|
||
|
// Process each contact type
|
||
|
['Registrant', 'Tech', 'Admin', 'AuxBilling'].forEach(type => {
|
||
|
const contactType = type.toLowerCase() as keyof typeof contacts;
|
||
|
const contactInfo = contacts[contactType];
|
||
|
|
||
|
if (contactInfo) {
|
||
|
// Ensure all required fields are present
|
||
|
this.validateContactInfo(contactInfo, type);
|
||
|
|
||
|
// Add each field to the params with the appropriate prefix
|
||
|
Object.entries(contactInfo).forEach(([field, value]) => {
|
||
|
if (value !== undefined && value !== null) {
|
||
|
params[`${type}${field}`] = String(value);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return params;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validate contact information to ensure required fields are present
|
||
|
* @param contactInfo Contact information to validate
|
||
|
* @param contactType Type of contact (Registrant, Tech, Admin, AuxBilling)
|
||
|
* @throws Error if required fields are missing
|
||
|
*/
|
||
|
private validateContactInfo(contactInfo: IContactInfo, contactType: string): void {
|
||
|
// Required fields for all contact types
|
||
|
const requiredFields = [
|
||
|
'FirstName',
|
||
|
'LastName',
|
||
|
'Address1',
|
||
|
'City',
|
||
|
'StateProvince',
|
||
|
'PostalCode',
|
||
|
'Country',
|
||
|
'Phone',
|
||
|
'EmailAddress'
|
||
|
];
|
||
|
|
||
|
// Check for missing required fields
|
||
|
const missingFields = requiredFields.filter(field => {
|
||
|
return !contactInfo[field as keyof IContactInfo];
|
||
|
});
|
||
|
|
||
|
if (missingFields.length > 0) {
|
||
|
throw new Error(
|
||
|
`Missing required fields for ${contactType} contact: ${missingFields.join(', ')}`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|