@chirpier/chirpier-js 0.1.5 → 0.2.0

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/index.ts CHANGED
@@ -1,181 +1,377 @@
1
- // Import necessary dependencies
2
1
  import axios, { AxiosInstance } from "axios";
3
2
  import axiosRetry from "axios-retry";
4
- import { Base64 } from "js-base64";
3
+ import dotenv from "dotenv";
5
4
  import {
6
5
  DEFAULT_RETRIES,
7
6
  DEFAULT_TIMEOUT,
8
7
  DEFAULT_BATCH_SIZE,
9
8
  DEFAULT_FLUSH_DELAY,
10
9
  MAX_QUEUE_SIZE,
10
+ DEFAULT_API_ENDPOINT,
11
+ DEFAULT_SERVICER_ENDPOINT,
11
12
  } from "./constants";
12
13
  import AsyncLock from "async-lock";
13
14
 
14
- // Define logging levels
15
- export enum LogLevel {
15
+ // Define logging levels as const enum for better tree-shaking
16
+ export const enum LogLevel {
16
17
  None = 0,
17
18
  Error = 1,
18
19
  Info = 2,
19
20
  Debug = 3,
20
21
  }
21
22
 
22
- // Define the options interface for Chirpier initialization
23
- interface Options {
24
- key: string;
25
- region?: "us-west" | "eu-west" | "asia-southeast";
23
+ export interface Config {
24
+ key?: string;
25
+ apiEndpoint?: string;
26
+ servicerEndpoint?: string;
26
27
  logLevel?: LogLevel;
28
+ retries?: number;
29
+ timeout?: number;
30
+ batchSize?: number;
31
+ flushDelay?: number;
32
+ maxQueueSize?: number;
27
33
  }
28
34
 
29
- // Define the Event interface for monitoring
30
- export interface Event {
31
- group_id: string;
32
- stream_name: string;
35
+ /**
36
+ * @deprecated Use Config.
37
+ */
38
+ export type Options = Config;
39
+
40
+ export interface Log {
41
+ agent_id?: string;
42
+ event: string;
33
43
  value: number;
44
+ meta?: unknown;
45
+ occurred_at?: string | Date;
46
+ }
47
+
48
+ export interface EventDefinition {
49
+ readonly event_id: string;
50
+ readonly agent_id?: string;
51
+ readonly event: string;
52
+ readonly title?: string;
53
+ readonly public: boolean;
54
+ readonly description?: string;
55
+ readonly unit?: string;
56
+ readonly semantic_class: string;
57
+ readonly default_aggregate: string;
58
+ readonly enabled: boolean;
59
+ readonly origin: string;
60
+ readonly archived_at?: string;
61
+ readonly created_at?: string;
34
62
  }
35
63
 
64
+ export interface Policy {
65
+ readonly policy_id: string;
66
+ readonly event_id: string;
67
+ readonly title: string;
68
+ readonly description?: string;
69
+ readonly channel: string;
70
+ readonly period: string;
71
+ readonly aggregate: string;
72
+ readonly condition: string;
73
+ readonly threshold: number;
74
+ readonly severity: string;
75
+ readonly enabled: boolean;
76
+ }
77
+
78
+ export interface Alert {
79
+ readonly alert_id: string;
80
+ readonly policy_id: string;
81
+ readonly event_id: string;
82
+ readonly agent_id?: string;
83
+ readonly event: string;
84
+ readonly title: string;
85
+ readonly period: string;
86
+ readonly aggregate: string;
87
+ readonly condition: string;
88
+ readonly threshold: number;
89
+ readonly severity: string;
90
+ readonly status: string;
91
+ readonly value: number;
92
+ readonly count: number;
93
+ readonly min: number;
94
+ readonly max: number;
95
+ readonly triggered_at?: string;
96
+ readonly acknowledged_at?: string;
97
+ readonly resolved_at?: string;
98
+ }
99
+
100
+ export interface AlertDelivery {
101
+ readonly attempt_id: string;
102
+ readonly alert_id: string;
103
+ readonly webhook_id?: string;
104
+ readonly channel: string;
105
+ readonly target: string;
106
+ readonly status: string;
107
+ readonly response_status?: number;
108
+ readonly error_message?: string;
109
+ readonly created_at: string;
110
+ }
111
+
112
+ export interface EventLogPoint {
113
+ readonly event_id: string;
114
+ readonly agent_id?: string;
115
+ readonly event: string;
116
+ readonly period: string;
117
+ readonly occurred_at: string;
118
+ readonly count: number;
119
+ readonly value: number;
120
+ readonly squares: number;
121
+ readonly min: number;
122
+ readonly max: number;
123
+ }
124
+
125
+ export interface PaginationOptions {
126
+ period?: "minute" | "hour" | "day";
127
+ limit?: number;
128
+ offset?: number;
129
+ }
130
+
131
+ export type DeliveryKind = "alert" | "test" | "all";
132
+
36
133
  // Custom error class for Chirpier-specific errors
37
134
  export class ChirpierError extends Error {
38
- constructor(message: string) {
135
+ constructor(
136
+ message: string,
137
+ public readonly code?: string
138
+ ) {
39
139
  super(message);
40
140
  this.name = "ChirpierError";
41
141
  Object.setPrototypeOf(this, ChirpierError.prototype);
42
142
  }
43
143
  }
44
144
 
45
- interface QueuedEvent {
46
- event: Event;
47
- timestamp: number;
145
+ interface QueuedLog {
146
+ readonly log: Log;
147
+ readonly timestamp: number;
48
148
  retryCount: number;
49
149
  }
50
150
 
51
- /**
52
- * Main Chirpier class for monitoring events.
53
- */
54
- export class Chirpier {
55
- private static instance: Chirpier | null = null;
151
+ export class Client {
56
152
  private readonly apiKey: string;
57
153
  private readonly apiEndpoint: string;
154
+ private readonly servicerEndpoint: string;
58
155
  private readonly retries: number;
59
156
  private readonly timeout: number;
60
157
  private readonly axiosInstance: AxiosInstance;
61
- private eventQueue: QueuedEvent[] = [];
158
+ private logQueue: QueuedLog[] = [];
62
159
  private readonly batchSize: number;
63
160
  private readonly flushDelay: number;
161
+ private readonly maxQueueSize: number;
64
162
  private flushTimeoutId: NodeJS.Timeout | null = null;
65
- private readonly queueLock = new AsyncLock();
66
- private readonly flushLock = new AsyncLock();
163
+ private readonly queueLock: AsyncLock;
164
+ private readonly flushLock: AsyncLock;
67
165
  private readonly logLevel: LogLevel;
68
166
 
69
- /**
70
- * Initializes a new instance of the Chirpier class.
71
- * @param options - Configuration options for the SDK.
72
- */
73
- private constructor(options: Options) {
74
- const { key, region = "events", logLevel = LogLevel.None } = options;
167
+ constructor(options: Config = {}) {
168
+ const {
169
+ key: providedKey,
170
+ apiEndpoint = DEFAULT_API_ENDPOINT,
171
+ servicerEndpoint = DEFAULT_SERVICER_ENDPOINT,
172
+ logLevel = LogLevel.None,
173
+ retries = DEFAULT_RETRIES,
174
+ timeout = DEFAULT_TIMEOUT,
175
+ batchSize = DEFAULT_BATCH_SIZE,
176
+ flushDelay = DEFAULT_FLUSH_DELAY,
177
+ maxQueueSize = MAX_QUEUE_SIZE,
178
+ } = options;
179
+
180
+ const key = resolveAPIKey(providedKey);
181
+
182
+ if (!key) {
183
+ throw new ChirpierError("API key is required", "INVALID_KEY");
184
+ }
75
185
 
76
- if (!key || typeof key !== "string") {
77
- throw new ChirpierError("API key is required and must be a string");
186
+ if (!isValidAPIKey(key)) {
187
+ throw new ChirpierError("Invalid API key: must start with 'chp_'", "INVALID_KEY");
78
188
  }
79
189
 
80
- if (typeof region !== "string" && !["us-west", "eu-west", "asia-southeast", "events"].includes(region)) {
81
- throw new ChirpierError("Region must be one of: us-west, eu-west, asia-southeast, events");
190
+ if (apiEndpoint !== undefined) {
191
+ if (typeof apiEndpoint !== "string" || apiEndpoint.trim().length === 0) {
192
+ throw new ChirpierError(
193
+ "apiEndpoint must be a non-empty string",
194
+ "INVALID_API_ENDPOINT"
195
+ );
196
+ }
197
+
198
+ let parsedURL: URL;
199
+ try {
200
+ parsedURL = new URL(apiEndpoint);
201
+ } catch {
202
+ throw new ChirpierError(
203
+ "apiEndpoint must be a valid absolute URL",
204
+ "INVALID_API_ENDPOINT"
205
+ );
206
+ }
207
+
208
+ if (parsedURL.protocol !== "https:" && parsedURL.protocol !== "http:") {
209
+ throw new ChirpierError(
210
+ "apiEndpoint must use http or https",
211
+ "INVALID_API_ENDPOINT"
212
+ );
213
+ }
82
214
  }
83
215
 
84
- this.apiEndpoint = `https://${region}.chirpier.co/v1.0/events`;
216
+ // Validate numeric options
217
+ if (retries < 0 || !Number.isInteger(retries)) {
218
+ throw new ChirpierError("Retries must be a non-negative integer", "INVALID_RETRIES");
219
+ }
220
+ if (timeout <= 0) {
221
+ throw new ChirpierError("Timeout must be positive", "INVALID_TIMEOUT");
222
+ }
223
+ if (batchSize <= 0 || !Number.isInteger(batchSize)) {
224
+ throw new ChirpierError("Batch size must be a positive integer", "INVALID_BATCH_SIZE");
225
+ }
226
+ if (flushDelay < 0) {
227
+ throw new ChirpierError("Flush delay must be non-negative", "INVALID_FLUSH_DELAY");
228
+ }
229
+ if (maxQueueSize <= 0 || !Number.isInteger(maxQueueSize)) {
230
+ throw new ChirpierError("Max queue size must be a positive integer", "INVALID_QUEUE_SIZE");
231
+ }
232
+
233
+ this.apiEndpoint = apiEndpoint ?? DEFAULT_API_ENDPOINT;
234
+ this.servicerEndpoint = servicerEndpoint ?? DEFAULT_SERVICER_ENDPOINT;
85
235
  this.apiKey = key;
86
- this.retries = DEFAULT_RETRIES;
87
- this.timeout = DEFAULT_TIMEOUT;
88
- this.batchSize = DEFAULT_BATCH_SIZE;
89
- this.flushDelay = DEFAULT_FLUSH_DELAY;
236
+ this.retries = retries;
237
+ this.timeout = timeout;
238
+ this.batchSize = batchSize;
239
+ this.flushDelay = flushDelay;
240
+ this.maxQueueSize = maxQueueSize;
90
241
  this.logLevel = logLevel;
91
242
 
92
- // Create axios instance with authorization header
243
+ this.queueLock = new AsyncLock({ maxPending: this.maxQueueSize });
244
+ this.flushLock = new AsyncLock({ maxPending: this.maxQueueSize });
245
+
93
246
  this.axiosInstance = axios.create({
94
247
  headers: { Authorization: `Bearer ${this.apiKey}` },
95
248
  timeout: this.timeout,
96
249
  });
97
250
 
98
- // Add the interceptor here
99
251
  this.axiosInstance.interceptors.response.use(
100
252
  (response) => response,
101
- (error) => {
102
- // Don't handle the error here; let axios-retry handle it
103
- return Promise.reject(error);
104
- }
253
+ (error) => Promise.reject(error)
105
254
  );
106
255
 
107
- // Apply axios-retry to your Axios instance
108
256
  axiosRetry(this.axiosInstance, {
109
257
  retries: this.retries,
110
258
  retryDelay: (retryCount) => {
111
- return Math.pow(2, retryCount) * 1000; // Exponential backoff starting at 1 second
259
+ const baseDelay = Math.pow(2, retryCount) * 1000;
260
+ const jitter = Math.random() * 0.3 * baseDelay;
261
+ return baseDelay + jitter;
112
262
  },
113
263
  retryCondition: (error) => {
114
- // Retry on network errors, 5xx errors, and 429 (Too Many Requests)
115
264
  return (
116
- axiosRetry.isNetworkError(error) ||
265
+ axiosRetry.isNetworkError(error) ||
117
266
  axiosRetry.isRetryableError(error) ||
118
- ((error.response && error.response.status) === 429)
267
+ (error.response && error.response.status) === 429
119
268
  );
120
269
  },
121
270
  shouldResetTimeout: true,
122
271
  });
123
272
  }
124
273
 
125
- /**
126
- * Gets the singleton instance of Chirpier, creating it if it doesn't exist.
127
- * @param options - Configuration options for the SDK.
128
- * @returns The Chirpier instance.
129
- */
130
- public static getInstance(options: Options): Chirpier | null {
131
- if (!Chirpier.instance && options.key) {
132
- Chirpier.instance = new Chirpier(options);
274
+ private isValidLog(log: Log): boolean {
275
+ const now = Date.now();
276
+ const oldestAllowed = now - 30 * 24 * 60 * 60 * 1000;
277
+ const newestAllowed = now + 24 * 60 * 60 * 1000;
278
+
279
+ if (typeof log.event !== "string" || log.event.trim().length === 0) {
280
+ return false;
133
281
  }
134
- return Chirpier.instance;
135
- }
136
-
137
- /**
138
- * Validates the event structure.
139
- * @param event - The event to validate.
140
- * @returns True if valid, false otherwise.
141
- */
142
- private isValidEvent(event: Event): boolean {
143
- return (
144
- typeof event.group_id === "string" &&
145
- /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
146
- event.group_id
147
- ) &&
148
- event.group_id.trim().length > 0 &&
149
- typeof event.stream_name === "string" &&
150
- event.stream_name.trim().length > 0 &&
151
- typeof event.value === "number"
152
- );
282
+
283
+ if (typeof log.value !== "number" || !Number.isFinite(log.value)) {
284
+ return false;
285
+ }
286
+
287
+ if (log.agent_id !== undefined && typeof log.agent_id !== "string") {
288
+ return false;
289
+ }
290
+
291
+ if (log.meta !== undefined) {
292
+ try {
293
+ const serializedMeta = JSON.stringify(log.meta);
294
+ if (serializedMeta === undefined) {
295
+ return false;
296
+ }
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ if (log.occurred_at !== undefined) {
303
+ const occurredAtMillis =
304
+ log.occurred_at instanceof Date
305
+ ? log.occurred_at.getTime()
306
+ : new Date(log.occurred_at).getTime();
307
+
308
+ if (!Number.isFinite(occurredAtMillis)) {
309
+ return false;
310
+ }
311
+
312
+ if (occurredAtMillis < oldestAllowed || occurredAtMillis > newestAllowed) {
313
+ return false;
314
+ }
315
+ }
316
+
317
+ return true;
153
318
  }
154
319
 
155
- /**
156
- * Monitors an event by adding it to the queue and scheduling a flush if necessary.
157
- * @param event - The event to monitor.
158
- */
159
- public async monitor(event: Event): Promise<void> {
160
- if (!this.isValidEvent(event)) {
161
- if (this.logLevel >= LogLevel.Debug) {
162
- console.debug("Invalid event format, dropping event:", event);
320
+ private normalizeLog(log: Log): Log {
321
+ const normalizedLog: Log = {
322
+ event: log.event.trim(),
323
+ value: log.value,
324
+ };
325
+
326
+ if (typeof log.agent_id === "string") {
327
+ const trimmedAgentID = log.agent_id.trim();
328
+ if (trimmedAgentID.length > 0) {
329
+ normalizedLog.agent_id = trimmedAgentID;
163
330
  }
164
- return; // Silently drop the event
165
331
  }
166
332
 
333
+ if (log.meta !== undefined) {
334
+ normalizedLog.meta = log.meta;
335
+ }
336
+
337
+ if (log.occurred_at !== undefined) {
338
+ const occurredAtDate =
339
+ log.occurred_at instanceof Date ? log.occurred_at : new Date(log.occurred_at);
340
+ normalizedLog.occurred_at = occurredAtDate.toISOString();
341
+ }
342
+
343
+ return normalizedLog;
344
+ }
345
+
346
+ public async log(log: Log): Promise<void> {
347
+ if (!this.isValidLog(log)) {
348
+ throw new ChirpierError(
349
+ "Invalid log format: event must not be empty, value must be a finite number, agent_id must be a string when provided, meta must be JSON-encodable, and occurred_at must be within the last 30 days and no more than 1 day in the future",
350
+ "INVALID_LOG"
351
+ );
352
+ }
353
+
354
+ const normalizedLog = this.normalizeLog(log);
355
+
356
+ let queueFull = false;
357
+
167
358
  await this.queueLock.acquire("queue", async () => {
168
- if (this.eventQueue.length >= MAX_QUEUE_SIZE) {
169
- if (this.logLevel >= LogLevel.Debug) {
170
- console.debug("Event queue is full, dropping event:", event);
171
- }
172
- return; // Silently drop the event
173
- }
359
+ if (this.logQueue.length >= this.maxQueueSize) {
360
+ queueFull = true;
361
+ return;
362
+ }
174
363
 
175
- this.eventQueue.push({ event, timestamp: Date.now(), retryCount: 0 });
364
+ this.logQueue.push({ log: normalizedLog, timestamp: Date.now(), retryCount: 0 });
176
365
  });
177
366
 
178
- if (this.eventQueue.length >= this.batchSize) {
367
+ if (queueFull) {
368
+ throw new ChirpierError(
369
+ `Log queue is full (max size: ${this.maxQueueSize})`,
370
+ "QUEUE_FULL"
371
+ );
372
+ }
373
+
374
+ if (this.logQueue.length >= this.batchSize) {
179
375
  await this.flushQueue();
180
376
  } else if (!this.flushTimeoutId) {
181
377
  this.flushTimeoutId = setTimeout(
@@ -185,142 +381,238 @@ export class Chirpier {
185
381
  }
186
382
  }
187
383
 
188
- /**
189
- * Flushes the event queue by sending all events to the API.
190
- */
191
384
  private async flushQueue(): Promise<void> {
192
- // Acquire the flush lock
193
385
  await this.flushLock.acquire("flush", async () => {
194
- let eventsToSend: QueuedEvent[] = [];
386
+ let logsToSend: QueuedLog[] = [];
195
387
 
196
- // Extract events from the queue under the queue lock
197
- await this.queueLock.acquire("eventQueue", async () => {
198
- if (this.eventQueue.length > 0) {
199
- eventsToSend = [...this.eventQueue];
200
- this.eventQueue = [];
388
+ await this.queueLock.acquire("logQueue", async () => {
389
+ if (this.logQueue.length > 0) {
390
+ logsToSend = [...this.logQueue];
391
+ this.logQueue = [];
201
392
  }
202
393
  });
203
394
 
204
- if (eventsToSend.length === 0) {
395
+ if (logsToSend.length === 0) {
205
396
  return;
206
397
  }
207
398
 
208
399
  try {
209
- // Clear any pending flush timeout
210
400
  if (this.flushTimeoutId) {
211
401
  clearTimeout(this.flushTimeoutId);
212
402
  this.flushTimeoutId = null;
213
403
  }
214
404
 
215
- // Attempt to send events
216
- await this.sendEvents(eventsToSend.map((qe) => qe.event));
405
+ await this.sendLogs(logsToSend.map((queuedLog) => queuedLog.log));
217
406
 
218
407
  if (this.logLevel >= LogLevel.Info) {
219
- console.info(`Successfully sent ${eventsToSend.length} events`);
408
+ console.info(`Successfully sent ${logsToSend.length} logs`);
220
409
  }
221
410
  } catch (error) {
222
- // Log failure
223
411
  if (this.logLevel >= LogLevel.Error) {
224
- console.error("Failed to send events:", error);
412
+ console.error("Failed to send logs:", error);
225
413
  }
226
414
 
227
- // Requeue failed events with retry count checks
228
- const retryableEvents: QueuedEvent[] = [];
229
- for (const queuedEvent of eventsToSend) {
230
- if (queuedEvent.retryCount >= this.retries) {
415
+ const retryableLogs: QueuedLog[] = [];
416
+ for (const queuedLog of logsToSend) {
417
+ if (queuedLog.retryCount >= this.retries) {
231
418
  if (this.logLevel >= LogLevel.Error) {
232
419
  console.error(
233
- `Dropping event after ${this.retries} retries:`,
234
- queuedEvent.event
420
+ `Dropping log after ${this.retries} retries:`,
421
+ queuedLog.log
235
422
  );
236
423
  }
237
- continue; // Skip adding this event back to the queue
424
+ continue;
238
425
  }
239
426
 
240
- // Increment retry count and add back to the queue
241
- queuedEvent.retryCount++;
242
- retryableEvents.push(queuedEvent);
427
+ queuedLog.retryCount++;
428
+ retryableLogs.push(queuedLog);
243
429
  }
244
430
 
245
- // Requeue remaining retryable events
246
- await this.queueLock.acquire("eventQueue", async () => {
247
- this.eventQueue = [...retryableEvents, ...this.eventQueue];
431
+ await this.queueLock.acquire("logQueue", async () => {
432
+ this.logQueue = [...retryableLogs, ...this.logQueue];
248
433
  });
249
434
  }
250
435
  });
251
436
  }
252
437
 
253
- /**
254
- * Sends multiple events to the API in a batch.
255
- * @param events - The array of events to send.
256
- */
257
- private async sendEvents(events: Event[]): Promise<void> {
258
- await this.axiosInstance.post(this.apiEndpoint, events);
438
+ private async sendLogs(logs: Log[]): Promise<void> {
439
+ await this.axiosInstance.post(this.apiEndpoint, logs);
259
440
  }
260
441
 
261
- // Stop the timeout and uninitialize the Chirpier instance
262
- public static async stop(): Promise<void> {
263
- if (!Chirpier.instance) {
264
- return;
265
- }
266
- if (Chirpier.instance.flushTimeoutId) {
267
- clearTimeout(Chirpier.instance.flushTimeoutId);
268
- Chirpier.instance.flushTimeoutId = null;
442
+ public async flush(): Promise<void> {
443
+ await this.flushQueue();
444
+ }
445
+
446
+ public async shutdown(): Promise<void> {
447
+ if (this.flushTimeoutId) {
448
+ clearTimeout(this.flushTimeoutId);
449
+ this.flushTimeoutId = null;
269
450
  }
270
- // Flush any remaining events in the queue
271
- await Chirpier.instance.flushQueue();
272
- // Uninitialize the Chirpier instance
273
- Chirpier.instance = null;
451
+
452
+ await this.flushQueue();
453
+ }
454
+
455
+ public async close(): Promise<void> {
456
+ await this.shutdown();
457
+ }
458
+
459
+ public async listEvents(): Promise<EventDefinition[]> {
460
+ const response = await this.axiosInstance.get<EventDefinition[]>(`${this.servicerEndpoint}/events`);
461
+ return response.data;
462
+ }
463
+
464
+ public async getEvent(eventID: string): Promise<EventDefinition> {
465
+ const response = await this.axiosInstance.get<EventDefinition>(`${this.servicerEndpoint}/events/${eventID}`);
466
+ return response.data;
467
+ }
468
+
469
+ public async updateEvent(
470
+ eventID: string,
471
+ payload: Partial<Omit<EventDefinition, "event_id" | "created_at">>
472
+ ): Promise<EventDefinition> {
473
+ const response = await this.axiosInstance.put<EventDefinition>(
474
+ `${this.servicerEndpoint}/events/${eventID}`,
475
+ payload
476
+ );
477
+ return response.data;
478
+ }
479
+
480
+ public async listPolicies(): Promise<Policy[]> {
481
+ const response = await this.axiosInstance.get<Policy[]>(`${this.servicerEndpoint}/policies`);
482
+ return response.data;
483
+ }
484
+
485
+ public async createPolicy(payload: Omit<Policy, "policy_id">): Promise<Policy> {
486
+ const response = await this.axiosInstance.post<Policy>(`${this.servicerEndpoint}/policies`, payload);
487
+ return response.data;
488
+ }
489
+
490
+ public async listAlerts(status?: string): Promise<Alert[]> {
491
+ const endpoint = status
492
+ ? `${this.servicerEndpoint}/alerts?status=${encodeURIComponent(status)}`
493
+ : `${this.servicerEndpoint}/alerts`;
494
+ const response = await this.axiosInstance.get<Alert[]>(endpoint);
495
+ return response.data;
274
496
  }
497
+
498
+ public async getAlertDeliveries(alertID: string, options: { limit?: number; offset?: number; kind?: DeliveryKind } = {}): Promise<AlertDelivery[]> {
499
+ const params = new URLSearchParams();
500
+ if (options.kind) {
501
+ params.set("kind", options.kind);
502
+ }
503
+ if (typeof options.limit === "number") {
504
+ params.set("limit", String(options.limit));
505
+ }
506
+ if (typeof options.offset === "number") {
507
+ params.set("offset", String(options.offset));
508
+ }
509
+ const suffix = params.toString() ? `?${params.toString()}` : "";
510
+ const response = await this.axiosInstance.get<AlertDelivery[]>(`${this.servicerEndpoint}/alerts/${alertID}/deliveries${suffix}`);
511
+ return response.data;
512
+ }
513
+
514
+ public async acknowledgeAlert(alertID: string): Promise<Alert> {
515
+ const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/acknowledge`);
516
+ return response.data;
517
+ }
518
+
519
+ public async archiveAlert(alertID: string): Promise<Alert> {
520
+ const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/archive`);
521
+ return response.data;
522
+ }
523
+
524
+ public async testWebhook(webhookID: string): Promise<void> {
525
+ await this.axiosInstance.post(`${this.servicerEndpoint}/webhooks/${webhookID}/test`);
526
+ }
527
+
528
+ public async getEventLogs(eventID: string, options: PaginationOptions = {}): Promise<EventLogPoint[]> {
529
+ const params = new URLSearchParams();
530
+ if (options.period) {
531
+ params.set("period", options.period);
532
+ }
533
+ if (typeof options.limit === "number") {
534
+ params.set("limit", String(options.limit));
535
+ }
536
+ if (typeof options.offset === "number") {
537
+ params.set("offset", String(options.offset));
538
+ }
539
+ const suffix = params.toString() ? `?${params.toString()}` : "";
540
+ const response = await this.axiosInstance.get<EventLogPoint[]>(`${this.servicerEndpoint}/events/${eventID}/logs${suffix}`);
541
+ return response.data;
542
+ }
543
+
544
+ public async resolveAlert(alertID: string): Promise<Alert> {
545
+ const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/resolve`);
546
+ return response.data;
547
+ }
548
+
275
549
  }
276
550
 
277
- /**
278
- * Decodes a base64url encoded string.
279
- * @param str - The base64url encoded string to decode.
280
- * @returns The decoded string.
281
- */
282
- function base64UrlDecode(str: string): string {
283
- // Replace '-' with '+' and '_' with '/'
284
- let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
285
- // Pad the base64 string
286
- const padding = base64.length % 4;
287
- if (padding !== 0) {
288
- base64 += "=".repeat(4 - padding);
289
- }
290
- return Base64.decode(base64);
551
+ let instance: Client | null = null;
552
+
553
+ export function createClient(config: Config = {}): Client {
554
+ return new Client(config);
291
555
  }
292
556
 
293
- /**
294
- * Validates if the provided token is a valid JWT.
295
- * @param token - The token to validate.
296
- * @returns True if valid, false otherwise.
297
- */
298
- function isValidJWT(token: string): boolean {
299
- const parts = token.split(".");
300
- if (parts.length !== 3) {
301
- return false;
557
+ function isNodeEnvironment(): boolean {
558
+ return typeof process !== "undefined" && !!(process.versions && process.versions.node);
559
+ }
560
+
561
+ function isValidAPIKey(token: string): boolean {
562
+ return token.startsWith("chp_") && token.length > "chp_".length;
563
+ }
564
+
565
+ function loadDotEnvKey(): string | undefined {
566
+ if (!isNodeEnvironment()) {
567
+ return undefined;
302
568
  }
569
+
303
570
  try {
304
- const header = JSON.parse(base64UrlDecode(parts[0]));
305
- const payload = JSON.parse(base64UrlDecode(parts[1]));
306
- return typeof header === "object" && typeof payload === "object";
307
- } catch (error) {
308
- console.error("Failed to validate JWT:", error);
309
- return false;
571
+ dotenv.config({ path: ".env", override: false });
572
+ } catch {
573
+ return undefined;
574
+ }
575
+
576
+ const envKey = process.env.CHIRPIER_API_KEY;
577
+ if (typeof envKey !== "string") {
578
+ return undefined;
579
+ }
580
+
581
+ const trimmedKey = envKey.trim();
582
+ return trimmedKey.length > 0 ? trimmedKey : undefined;
583
+ }
584
+
585
+ function resolveAPIKey(providedKey?: string): string | undefined {
586
+ if (typeof providedKey === "string" && providedKey.trim().length > 0) {
587
+ return providedKey.trim();
310
588
  }
589
+
590
+ if (typeof process !== "undefined" && process.env && typeof process.env.CHIRPIER_API_KEY === "string") {
591
+ const envKey = process.env.CHIRPIER_API_KEY.trim();
592
+ if (envKey.length > 0) {
593
+ return envKey;
594
+ }
595
+ }
596
+
597
+ return loadDotEnvKey();
311
598
  }
312
599
 
313
- /**
314
- * Initializes the Chirpier SDK.
315
- * @param options - Configuration options for the SDK.
316
- */
317
- export function initialize(options: Options): void {
318
- if (!isValidJWT(options.key)) {
319
- throw new ChirpierError("Invalid API key: Not a valid JWT");
600
+ export function initialize(options: Config = {}): void {
601
+ const resolvedKey = resolveAPIKey(options.key);
602
+ if (!resolvedKey) {
603
+ throw new ChirpierError("API key is required", "INVALID_KEY");
604
+ }
605
+
606
+ if (!isValidAPIKey(resolvedKey)) {
607
+ throw new ChirpierError("Invalid API key: must start with 'chp_'", "INVALID_KEY");
608
+ }
609
+
610
+ if (instance) {
611
+ return;
320
612
  }
321
613
 
322
614
  try {
323
- Chirpier.getInstance(options);
615
+ instance = new Client({ ...options, key: resolvedKey });
324
616
  } catch (error) {
325
617
  if (error instanceof ChirpierError) {
326
618
  if (options.logLevel && options.logLevel >= LogLevel.Error) {
@@ -338,19 +630,33 @@ export function initialize(options: Options): void {
338
630
  }
339
631
  }
340
632
 
341
- /**
342
- * Monitors an event using the Chirpier SDK.
343
- * @param event - The event to monitor.
344
- */
345
- export function monitor(event: Event): void {
346
- const instance = Chirpier.getInstance({} as Options);
633
+ export async function logEvent(log: Log): Promise<void> {
634
+ if (!instance) {
635
+ throw new ChirpierError(
636
+ "Chirpier SDK is not initialized. Please call initialize() first.",
637
+ "NOT_INITIALIZED"
638
+ );
639
+ }
640
+
641
+ await instance.log(log);
642
+ }
643
+
644
+ export async function stop(): Promise<void> {
645
+ if (!instance) {
646
+ return;
647
+ }
648
+
649
+ await instance.shutdown();
650
+ instance = null;
651
+ }
652
+
653
+ export async function flush(): Promise<void> {
347
654
  if (!instance) {
348
655
  throw new ChirpierError(
349
- "Chirpier SDK is not initialized. Please call initialize() first."
656
+ "Chirpier SDK is not initialized. Please call initialize() first.",
657
+ "NOT_INITIALIZED"
350
658
  );
351
659
  }
352
660
 
353
- instance.monitor(event).catch((error) => {
354
- console.error("Error in monitor function:", error);
355
- });
661
+ await instance.flush();
356
662
  }