@ecodrix/erix-api 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts DELETED
@@ -1,305 +0,0 @@
1
- import repl from "node:repl";
2
- import { Command } from "commander";
3
- import dotenv from "dotenv";
4
- import pc from "picocolors";
5
- import { Ecodrix } from "./core";
6
-
7
- // Setup environment
8
- dotenv.config();
9
-
10
- // Build-time constants injected by tsup
11
- declare const process: any;
12
- const VERSION = process.env.SDK_VERSION || "1.0.2";
13
- const NAME = process.env.SDK_NAME || "@ecodrix/erix-api";
14
- const DESCRIPTION = process.env.SDK_DESCRIPTION || "Official ECODrIx SDK CLI";
15
-
16
- const program = new Command();
17
-
18
- program.name("erix").description(DESCRIPTION).version(VERSION);
19
-
20
- /**
21
- * Helper to initialize the Ecodrix client from environment or flags.
22
- */
23
- function getClient(options: any): Ecodrix {
24
- const apiKey = options.key || process.env.ECOD_API_KEY;
25
- const clientCode = options.client || process.env.ECOD_CLIENT_CODE;
26
-
27
- if (!apiKey) {
28
- console.error(pc.red("Error: API Key is missing."));
29
- console.log(pc.yellow("Set ECOD_API_KEY environment variable or use --key <key>"));
30
- process.exit(1);
31
- }
32
-
33
- return new Ecodrix({
34
- apiKey,
35
- clientCode,
36
- baseUrl: options.baseUrl || process.env.ECOD_BASE_URL,
37
- });
38
- }
39
-
40
- program
41
- .option("-k, --key <key>", "ECODrIx API Key")
42
- .option("-c, --client <code...", "Tenant Client Code")
43
- .option("--base-url <url>", "API Base URL override");
44
-
45
- // --- WHOAMI ---
46
- program
47
- .command("whoami")
48
- .description("Verify current authentication and tenant status")
49
- .action(async (options, command) => {
50
- const globalOptions = command.parent.opts();
51
- const ecod = getClient(globalOptions);
52
-
53
- console.log(pc.cyan("Checking connection to ECODrIx Platform..."));
54
- try {
55
- const me = (await ecod.request("GET", "/api/saas/me/profile")) as any;
56
- console.log(pc.green("✔ Authenticated successfully!"));
57
- console.log(`${pc.bold("User ID:")} ${me.id}`);
58
- console.log(`${pc.bold("Organisation:")} ${me.organisation?.name || "N/A"}`);
59
- if (globalOptions.client) {
60
- console.log(`${pc.bold("Tenant Code:")} ${pc.magenta(globalOptions.client)}`);
61
- }
62
- } catch (error: any) {
63
- console.error(pc.red("✖ Authentication failed"));
64
- console.error(pc.dim(error.message));
65
- process.exit(1);
66
- }
67
- });
68
-
69
- // --- WHATSAPP ---
70
- const whatsapp = program.command("whatsapp").description("WhatsApp Business API operations");
71
-
72
- whatsapp
73
- .command("send-template")
74
- .description("Send a WhatsApp template message")
75
- .argument("<phone>", "Recipient phone number")
76
- .argument("<template>", "Template name")
77
- .option("-v, --vars <json>", "Template variables as JSON string", "[]")
78
- .action(async (phone, template, options, command) => {
79
- const globalOptions = command.parent.parent.opts();
80
- const ecod = getClient(globalOptions);
81
-
82
- try {
83
- const vars = JSON.parse(options.vars);
84
- console.log(pc.cyan(`Sending template '${template}' to ${phone}...`));
85
-
86
- const result = (await ecod.whatsapp.messages.sendTemplate({
87
- to: phone,
88
- templateName: template,
89
- language: "en_US",
90
- variables: vars,
91
- })) as any;
92
-
93
- console.log(pc.green("✔ Message sent successfully!"));
94
- console.log(pc.dim(`ID: ${result?.id || "N/A"}`));
95
- } catch (error: any) {
96
- console.error(pc.red("✖ Failed to send message"));
97
- console.error(pc.dim(error.message));
98
- }
99
- });
100
-
101
- // --- CRM ---
102
- const crm = program.command("crm").description("CRM and Lead management");
103
-
104
- crm
105
- .command("leads")
106
- .description("List recent leads")
107
- .option("-l, --limit <number>", "Number of leads to fetch", "10")
108
- .option("-s, --status <status>", "Filter by lead status")
109
- .option("-p, --pipeline <id>", "Filter by pipeline ID")
110
- .option("-q, --search <query>", "Search by name or email")
111
- .action(async (options, command) => {
112
- const globalOptions = command.parent.parent.opts();
113
- const ecod = getClient(globalOptions);
114
-
115
- try {
116
- const limit = Number.parseInt(options.limit);
117
- console.log(pc.cyan(`Fetching last ${limit} leads...`));
118
-
119
- const queryParams: any = { limit };
120
- if (options.status) queryParams.status = options.status;
121
- if (options.pipeline) queryParams.pipelineId = options.pipeline;
122
- if (options.search) queryParams.search = options.search;
123
-
124
- const response: any = await ecod.crm.leads.list(queryParams);
125
- const leads = Array.isArray(response.data)
126
- ? response.data
127
- : Array.isArray(response)
128
- ? response
129
- : [];
130
-
131
- if (leads.length === 0) {
132
- console.log(pc.yellow("No leads found."));
133
- return;
134
- }
135
-
136
- console.table(
137
- leads.map((l: any) => ({
138
- ID: l.id || l._id,
139
- Name: `${l.firstName} ${l.lastName || ""}`.trim(),
140
- Phone: l.phone,
141
- Status: l.status,
142
- Score: l.score || 0,
143
- Created: new Date(l.createdAt).toLocaleDateString(),
144
- })),
145
- );
146
- } catch (error: any) {
147
- console.error(pc.red("✖ Failed to fetch leads"));
148
- console.error(pc.dim(error.message));
149
- }
150
- });
151
-
152
- crm
153
- .command("pipelines")
154
- .description("List all CRM pipelines")
155
- .action(async (options, command) => {
156
- const globalOptions = command.parent.parent.opts();
157
- const ecod = getClient(globalOptions);
158
-
159
- try {
160
- console.log(pc.cyan("Fetching pipelines..."));
161
- const response: any = await ecod.crm.pipelines.list();
162
- const pipelines = response.data || response || [];
163
-
164
- if (pipelines.length === 0) {
165
- console.log(pc.yellow("No pipelines found."));
166
- return;
167
- }
168
-
169
- console.table(
170
- pipelines.map((p: any) => ({
171
- ID: p.id || p._id,
172
- Name: p.name,
173
- Default: p.isDefault ? "Yes" : "No",
174
- Stages: p.stages?.length || 0,
175
- })),
176
- );
177
- } catch (error: any) {
178
- console.error(pc.red("✖ Failed to fetch pipelines"));
179
- console.error(pc.dim(error.message));
180
- }
181
- });
182
-
183
- // --- ANALYTICS ---
184
- const analytics = program.command("analytics").description("Business Intelligence Analytics");
185
-
186
- analytics
187
- .command("overview")
188
- .description("Get high-level CRM KPIs")
189
- .option("-r, --range <range>", "Date range (e.g., 24h, 7d, 30d, 365d)", "30d")
190
- .action(async (options, command) => {
191
- const globalOptions = command.parent.parent.opts();
192
- const ecod = getClient(globalOptions);
193
-
194
- try {
195
- console.log(pc.cyan(`Fetching overview metrics for last ${options.range}...`));
196
- const response: any = await ecod.crm.analytics.overview({ range: options.range });
197
- const data = response.data || response;
198
-
199
- console.log(`\n${pc.bold("OVERVIEW KPIs:")}`);
200
- console.log(`Total Leads: ${pc.green(data.totalLeads || 0)}`);
201
- console.log(`Open Value: ${pc.yellow(`$${(data.openValue || 0).toLocaleString()}`)}`);
202
- console.log(`Won Revenue: ${pc.green(`$${(data.wonRevenue || 0).toLocaleString()}`)}`);
203
- console.log(`Avg Score: ${pc.blue(data.avgScore?.toFixed(1) || 0)}`);
204
- console.log(`Conversion: ${pc.magenta(`${(data.conversionRate || 0).toFixed(2)}%`)}\n`);
205
- } catch (error: any) {
206
- console.error(pc.red("✖ Failed to fetch analytics overview"));
207
- console.error(pc.dim(error.message));
208
- }
209
- });
210
-
211
- // --- WEBHOOKS ---
212
- const webhooks = program.command("webhooks").description("Webhook utility tools");
213
-
214
- webhooks
215
- .command("verify")
216
- .description("Verify a cryptographic webhook signature")
217
- .argument("<payload>", "The raw request body string")
218
- .argument("<signature>", "The 'x-ecodrix-signature' header value")
219
- .argument("<secret>", "Your webhook signing secret")
220
- .action(async (payload, signature, secret, options, command) => {
221
- const globalOptions = command.parent.parent.opts();
222
- const ecod = getClient(globalOptions);
223
-
224
- try {
225
- await ecod.webhooks.constructEvent(payload, signature, secret);
226
- console.log(pc.green("✔ Signature is VALID"));
227
- } catch (error: any) {
228
- console.error(pc.red("✖ Error during verification"));
229
- console.error(pc.dim(error.message));
230
- }
231
- });
232
-
233
- // --- SHELL (REPL) ---
234
- program
235
- .command("shell")
236
- .alias("repl")
237
- .description("Start an interactive SDK shell")
238
- .action(async (options, command) => {
239
- const globalOptions = command.parent.opts();
240
- const ecod = getClient(globalOptions);
241
-
242
- console.log(pc.magenta(pc.bold("\nWelcome to the Erix Interactive Shell")));
243
- console.log(pc.dim(`SDK Version: ${VERSION}`));
244
- console.log(pc.dim("The 'ecod' client is pre-initialized and ready.\n"));
245
-
246
- const r = repl.start({
247
- prompt: pc.cyan("erix > "),
248
- useColors: true,
249
- });
250
-
251
- // Load resources into REPL context for immediate access
252
- r.context.ecod = ecod;
253
- r.context.whatsapp = ecod.whatsapp;
254
- r.context.crm = ecod.crm;
255
- r.context.meet = ecod.meet;
256
- r.context.media = ecod.media;
257
-
258
- r.on("exit", () => {
259
- console.log(pc.yellow("\nGoodbye!"));
260
- process.exit(0);
261
- });
262
- });
263
-
264
- // --- COMPLETION ---
265
- program
266
- .command("completion")
267
- .description("Generate Bash auto-completion script")
268
- .action(() => {
269
- const script = `
270
- # Erix Bash Completion
271
- _erix_completions() {
272
- local cur opts
273
- COMPREPLY=()
274
- cur="\${COMP_WORDS[COMP_CWORD]}"
275
- opts="whoami whatsapp crm analytics webhooks shell completion"
276
-
277
- if [[ \${COMP_CWORD} -eq 1 ]] ; then
278
- COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
279
- return 0
280
- fi
281
-
282
- # Simple sub-command completion
283
- case "\${COMP_WORDS[1]}" in
284
- whatsapp)
285
- COMPREPLY=( $(compgen -W "send-template" -- \${cur}) )
286
- ;;
287
- crm)
288
- COMPREPLY=( $(compgen -W "leads pipelines" -- \${cur}) )
289
- ;;
290
- analytics)
291
- COMPREPLY=( $(compgen -W "overview" -- \${cur}) )
292
- ;;
293
- webhooks)
294
- COMPREPLY=( $(compgen -W "verify" -- \${cur}) )
295
- ;;
296
- esac
297
- }
298
- complete -F _erix_completions erix
299
- `;
300
- console.log(script.trim());
301
- console.error(pc.yellow('\n# To enable, run: eval "$(erix completion)"'));
302
- console.error(pc.dim("# Or add it to your ~/.bashrc: erix completion >> ~/.bashrc"));
303
- });
304
-
305
- program.parse();
package/src/core.ts DELETED
@@ -1,359 +0,0 @@
1
- import axios, { type AxiosInstance, type Method } from "axios";
2
- import axiosRetry from "axios-retry";
3
- import { type Socket, io } from "socket.io-client";
4
- import { APIError, AuthenticationError } from "./error";
5
- import { CRM } from "./resources/crm";
6
- import { Email } from "./resources/email";
7
- import { EventsResource } from "./resources/events";
8
- import { Health } from "./resources/health";
9
- import { Logs } from "./resources/logs";
10
- import { Marketing } from "./resources/marketing";
11
- import { Media } from "./resources/media";
12
- import { Meetings } from "./resources/meet";
13
- import { Notifications } from "./resources/notifications";
14
- import { Queue } from "./resources/queue";
15
- import { Storage } from "./resources/storage";
16
- import { Webhooks } from "./resources/webhooks";
17
- import { WhatsApp } from "./resources/whatsapp";
18
-
19
- declare const process: any;
20
-
21
- /** @internal The canonical ECODrIx backend URL — not exposed to SDK consumers. */
22
- const ECOD_API_BASE = "https://api.ecodrix.com";
23
-
24
- /**
25
- * Configuration options for the Ecodrix client.
26
- *
27
- * Minimum required: `apiKey` and `clientCode`.
28
- * The backend URL is managed internally and should not need to be changed.
29
- */
30
- export interface EcodrixOptions {
31
- /**
32
- * Your ECODrIx Platform API key.
33
- * Obtain this from the ECODrIx dashboard under Settings → API Keys.
34
- * @example "ecod_live_sk_..."
35
- */
36
- apiKey: string;
37
-
38
- /**
39
- * Your tenant ID (Client Code).
40
- * This scopes all API requests to your specific organisation.
41
- * @example "ERIX_CLNT_JHBJHF"
42
- */
43
- clientCode?: string;
44
-
45
- /**
46
- * @internal Override Socket.io URL for testing/staging.
47
- * Not documented — internal use only.
48
- */
49
- socketUrl?: string;
50
-
51
- /**
52
- * @internal Override the backend API URL. Used by the CLI and internal tests only.
53
- * Host projects must never set this — the production URL is hardcoded internally.
54
- */
55
- baseUrl?: string;
56
- }
57
-
58
- /**
59
- * The primary entry point for the ECODrIx SDK.
60
- *
61
- * Initialise once with your credentials and use the namespaced resources
62
- * to interact with every part of the platform.
63
- *
64
- * @example
65
- * ```typescript
66
- * import { Ecodrix } from "@ecodrix/erix-api";
67
- *
68
- * const ecod = new Ecodrix({
69
- * apiKey: process.env.ECOD_API_KEY!,
70
- * clientCode: "ERIX_CLNT_JHBJHF",
71
- * });
72
- *
73
- * await ecod.whatsapp.messages.send({ to: "+91...", text: "Hello!" });
74
- * const lead = await ecod.crm.leads.create({ firstName: "Alice", phone: "+91..." });
75
- * ```
76
- */
77
- export class Ecodrix {
78
- /**
79
- * @internal Axios HTTP client for making API requests.
80
- */
81
- private readonly client: AxiosInstance;
82
- /**
83
- * @internal Socket.io client for real-time events.
84
- */
85
- private readonly socket: Socket;
86
-
87
- /**
88
- * The tenant client code this SDK instance is scoped to.
89
- * Useful for components that need to read the clientCode back
90
- * from a context-provided SDK instance.
91
- */
92
- public readonly clientCode: string | undefined;
93
-
94
- /** WhatsApp messaging and conversation management. */
95
- public readonly whatsapp: WhatsApp;
96
-
97
- /** CRM resources — Leads and related sub-resources. */
98
- public readonly crm: CRM;
99
-
100
- /** Cloudflare R2-backed media storage. */
101
- public readonly media: Media;
102
-
103
- /** Google Meet appointment scheduling. */
104
- public readonly meet: Meetings;
105
-
106
- /** Automation execution logs and provider webhook callbacks. */
107
- public readonly notifications: Notifications;
108
-
109
- /** Outbound email marketing engine and template management. */
110
- public readonly email: Email;
111
-
112
- /** Platform-wide execution logs and audit trails. */
113
- public readonly logs: Logs;
114
-
115
- /** Lead events and workflow automation triggers. */
116
- public readonly events: EventsResource;
117
-
118
- /** Cryptographic webhook signature verification. */
119
- public readonly webhooks: Webhooks;
120
-
121
- /** Tenant Cloud Storage mapping. */
122
- public readonly storage: Storage;
123
-
124
- /** Email and SMS Marketing Campaigns. */
125
- public readonly marketing: Marketing;
126
-
127
- /** Platform and tenant health diagnostics. */
128
- public readonly health: Health;
129
-
130
- /** Background job queue management. */
131
- public readonly queue: Queue;
132
-
133
- constructor(options: EcodrixOptions) {
134
- if (!options.apiKey) {
135
- throw new AuthenticationError("API Key is required");
136
- }
137
-
138
- this.clientCode = options.clientCode?.toUpperCase();
139
-
140
- // @internal: options.baseUrl is available for CLI/test use only.
141
- // Host projects hardcode to prod — they never set baseUrl.
142
- const baseUrl = options.baseUrl ?? ECOD_API_BASE;
143
- const socketUrl = options.socketUrl || baseUrl;
144
-
145
- const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
146
- const runtime = isBrowser
147
- ? "browser"
148
- : typeof process !== "undefined"
149
- ? `node ${process.version}`
150
- : "unknown";
151
- const os = isBrowser
152
- ? globalThis.navigator?.userAgent || "browser"
153
- : typeof process !== "undefined"
154
- ? process.platform
155
- : "unknown";
156
-
157
- this.client = axios.create({
158
- baseURL: baseUrl,
159
- headers: {
160
- "x-api-key": options.apiKey,
161
- "x-client-code": options.clientCode?.toUpperCase(),
162
- "Content-Type": "application/json",
163
- "x-ecodrix-client-agent": JSON.stringify({
164
- sdk_version: "1.0.0", // Can be auto-injected during build in future
165
- runtime,
166
- os,
167
- }),
168
- },
169
- });
170
-
171
- // Make the client completely bulletproof for execution from external projects.
172
- // It will automatically handle network blips, 502 Bad Gateways, and 429 Rate Limits.
173
- axiosRetry(this.client, {
174
- retries: 3,
175
- retryDelay: axiosRetry.exponentialDelay,
176
- retryCondition: (error) => {
177
- return (
178
- axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status === 429
179
- );
180
- },
181
- onRetry: (retryCount, error, requestConfig) => {
182
- const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
183
- if (isDev) {
184
- console.warn(
185
- `[ECODrIx SDK] Retrying request (${retryCount}/3): ${requestConfig.method?.toUpperCase()} ${requestConfig.url}. Reason: ${error.message}`,
186
- );
187
- }
188
- },
189
- });
190
-
191
- // Initialise resources
192
- this.whatsapp = new WhatsApp(this.client);
193
- this.crm = new CRM(this.client);
194
- this.media = new Media(this.client);
195
- this.meet = new Meetings(this.client);
196
- this.notifications = new Notifications(this.client);
197
- this.email = new Email(this.client);
198
- this.logs = new Logs(this.client);
199
- this.events = new EventsResource(this.client);
200
- this.webhooks = new Webhooks();
201
- this.storage = new Storage(this.client);
202
- this.marketing = new Marketing(this.client);
203
- this.health = new Health(this.client);
204
- this.queue = new Queue(this.client);
205
-
206
- // Establish persistent Socket.io connection
207
- this.socket = io(socketUrl, {
208
- extraHeaders: {
209
- "x-api-key": options.apiKey,
210
- "x-client-code": options.clientCode?.toUpperCase() || "",
211
- },
212
- });
213
-
214
- this.setupSocket(options.clientCode);
215
-
216
- // ─── Wappalyzer & Technology Detection ─────────────────────────────────────
217
- if (isBrowser) {
218
- const footprint = {
219
- version: "1.2.2",
220
- clientCode: options.clientCode,
221
- initializedAt: new Date().toISOString(),
222
- };
223
- // Standard identifying global
224
- (window as any).__ECODRIX_SDK__ = footprint;
225
- // Ergonomic access for developers (as attempted by the user)
226
- if (!(window as any).ecodrix) {
227
- (window as any).ecodrix = this;
228
- }
229
- }
230
- }
231
-
232
- /**
233
- * Join a specific real-time room (e.g., a conversation or a lead).
234
- *
235
- * @param roomId - The unique identifier for the room.
236
- */
237
- public joinRoom(roomId: string) {
238
- this.socket.emit("join-room", roomId);
239
- }
240
-
241
- /**
242
- * Leave a previously joined real-time room.
243
- *
244
- * @param roomId - The unique identifier for the room.
245
- */
246
- public leaveRoom(roomId: string) {
247
- this.socket.emit("leave-room", roomId);
248
- }
249
-
250
- private setupSocket(clientCode?: string) {
251
- this.socket.on("connect", () => {
252
- if (clientCode) {
253
- // Join the tenant-scoped room to receive only relevant events.
254
- this.socket.emit("join-room", clientCode.toUpperCase());
255
- }
256
- });
257
- }
258
-
259
- /**
260
- * Subscribe to a real-time event emitted by the ECODrIx platform.
261
- *
262
- * The SDK maintains a persistent Socket.io connection. Events are
263
- * scoped to your `clientCode` — you will only receive events for
264
- * your own tenant.
265
- *
266
- * **Standard events:**
267
- * - `new_message` — inbound WhatsApp message (includes conversation and message payload)
268
- * - `message_sent` — outbound message successfully sent
269
- * - `message_status_update` — WhatsApp message status change (delivered, read, failed)
270
- * - `conversation_updated` — metadata change (unread count, last message, status)
271
- * - `message_updated` — real-time updates for reactions or media processing
272
- * - `notification:new` — new system or CRM notification
273
- * - `workflow-run-update` — automation execution progress
274
- * - `meet.scheduled` — Google Meet appointment booked
275
- *
276
- * @param event - The event name to subscribe to.
277
- * @param callback - The handler function invoked when the event fires.
278
- * @returns `this` for method chaining.
279
- *
280
- * @example
281
- * ```typescript
282
- * ecod
283
- * .on("whatsapp.message_received", (msg) => console.log(msg.body))
284
- * .on("automation.failed", (err) => alertTeam(err));
285
- * ```
286
- */
287
- public on(event: string, callback: (...args: any[]) => void): this {
288
- this.socket.on(event, callback);
289
- return this;
290
- }
291
-
292
- /**
293
- * Gracefully disconnect the real-time Socket.io connection.
294
- * Call this when shutting down your server or when the client is
295
- * no longer needed to free up resources.
296
- *
297
- * @example
298
- * ```typescript
299
- * process.on("SIGTERM", () => ecod.disconnect());
300
- * ```
301
- */
302
- public disconnect() {
303
- this.socket.disconnect();
304
- }
305
-
306
- /**
307
- * Remove a previously registered event listener.
308
- * Always call this in cleanup (e.g. React `useEffect` return) to prevent
309
- * memory leaks and duplicate handlers after reconnections.
310
- *
311
- * @param event - The event name to unsubscribe from.
312
- * @param callback - The exact handler reference passed to `.on()`.
313
- * @returns `this` for method chaining.
314
- */
315
- public off(event: string, callback: (...args: any[]) => void): this {
316
- this.socket.off(event, callback);
317
- return this;
318
- }
319
-
320
- /**
321
- * Raw Execution Escape-Hatch.
322
- * Send an authenticated HTTP request directly to the ECODrIx backend from ANY external project.
323
- *
324
- * This is extremely powerful giving you full unrestricted access to make calls
325
- * against new, experimental, or completely custom backend APIs while still benefitting
326
- * from the SDK's built-in authentication and automatic `axios-retry` logic.
327
- *
328
- * @example
329
- * ```typescript
330
- * const { data } = await ecod.request("POST", "/api/saas/experimental-feature", { flag: true });
331
- * console.log(data);
332
- * ```
333
- */
334
- public async request<T = any>(
335
- method: Method,
336
- path: string,
337
- data?: any,
338
- params?: any,
339
- ): Promise<T> {
340
- try {
341
- const response = await this.client.request<T>({
342
- method,
343
- url: path,
344
- data,
345
- params,
346
- });
347
- return response.data;
348
- } catch (error: any) {
349
- if (error.response) {
350
- throw new APIError(
351
- error.response.data?.message || error.response.data?.error || "Raw Execution Failed",
352
- error.response.status,
353
- error.response.data?.code,
354
- );
355
- }
356
- throw new APIError(error.message || "Network Error");
357
- }
358
- }
359
- }