@chirpier/chirpier-js 0.1.6 → 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,126 +1,266 @@
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
- maxPending: MAX_QUEUE_SIZE,
67
- });
68
- private readonly flushLock = new AsyncLock({
69
- maxPending: MAX_QUEUE_SIZE,
70
- });
163
+ private readonly queueLock: AsyncLock;
164
+ private readonly flushLock: AsyncLock;
71
165
  private readonly logLevel: LogLevel;
72
166
 
73
- /**
74
- * Initializes a new instance of the Chirpier class.
75
- * @param options - Configuration options for the SDK.
76
- */
77
- private constructor(options: Options) {
78
- const { key, region = "eu-west", 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
+ }
79
185
 
80
- if (!key || typeof key !== "string") {
81
- 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");
82
188
  }
83
189
 
84
- if (
85
- typeof region !== "string" &&
86
- !["us-west", "eu-west", "asia-southeast"].includes(region)
87
- ) {
88
- throw new ChirpierError(
89
- "Region must be one of: us-west, eu-west, asia-southeast"
90
- );
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
+ }
91
214
  }
92
215
 
93
- 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;
94
235
  this.apiKey = key;
95
- this.retries = DEFAULT_RETRIES;
96
- this.timeout = DEFAULT_TIMEOUT;
97
- this.batchSize = DEFAULT_BATCH_SIZE;
98
- 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;
99
241
  this.logLevel = logLevel;
100
242
 
101
- // Create axios instance with authorization header
243
+ this.queueLock = new AsyncLock({ maxPending: this.maxQueueSize });
244
+ this.flushLock = new AsyncLock({ maxPending: this.maxQueueSize });
245
+
102
246
  this.axiosInstance = axios.create({
103
247
  headers: { Authorization: `Bearer ${this.apiKey}` },
104
248
  timeout: this.timeout,
105
249
  });
106
250
 
107
- // Add the interceptor here
108
251
  this.axiosInstance.interceptors.response.use(
109
252
  (response) => response,
110
- (error) => {
111
- // Don't handle the error here; let axios-retry handle it
112
- return Promise.reject(error);
113
- }
253
+ (error) => Promise.reject(error)
114
254
  );
115
255
 
116
- // Apply axios-retry to your Axios instance
117
256
  axiosRetry(this.axiosInstance, {
118
257
  retries: this.retries,
119
258
  retryDelay: (retryCount) => {
120
- 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;
121
262
  },
122
263
  retryCondition: (error) => {
123
- // Retry on network errors, 5xx errors, and 429 (Too Many Requests)
124
264
  return (
125
265
  axiosRetry.isNetworkError(error) ||
126
266
  axiosRetry.isRetryableError(error) ||
@@ -131,60 +271,107 @@ export class Chirpier {
131
271
  });
132
272
  }
133
273
 
134
- /**
135
- * Gets the singleton instance of Chirpier, creating it if it doesn't exist.
136
- * @param options - Configuration options for the SDK.
137
- * @returns The Chirpier instance.
138
- */
139
- public static getInstance(options: Options): Chirpier | null {
140
- if (!Chirpier.instance && options.key) {
141
- 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;
142
281
  }
143
- return Chirpier.instance;
144
- }
145
-
146
- /**
147
- * Validates the event structure.
148
- * @param event - The event to validate.
149
- * @returns True if valid, false otherwise.
150
- */
151
- private isValidEvent(event: Event): boolean {
152
- return (
153
- typeof event.group_id === "string" &&
154
- /^[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(
155
- event.group_id
156
- ) &&
157
- event.group_id.trim().length > 0 &&
158
- typeof event.stream_name === "string" &&
159
- event.stream_name.trim().length > 0 &&
160
- typeof event.value === "number"
161
- );
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;
162
318
  }
163
319
 
164
- /**
165
- * Monitors an event by adding it to the queue and scheduling a flush if necessary.
166
- * @param event - The event to monitor.
167
- */
168
- public async monitor(event: Event): Promise<void> {
169
- if (!this.isValidEvent(event)) {
170
- if (this.logLevel >= LogLevel.Debug) {
171
- 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;
172
330
  }
173
- return; // Silently drop the event
174
331
  }
175
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
+
176
358
  await this.queueLock.acquire("queue", async () => {
177
- if (this.eventQueue.length >= MAX_QUEUE_SIZE) {
178
- if (this.logLevel >= LogLevel.Debug) {
179
- console.debug("Event queue is full, dropping event:", event);
180
- }
181
- return; // Silently drop the event
359
+ if (this.logQueue.length >= this.maxQueueSize) {
360
+ queueFull = true;
361
+ return;
182
362
  }
183
363
 
184
- this.eventQueue.push({ event, timestamp: Date.now(), retryCount: 0 });
364
+ this.logQueue.push({ log: normalizedLog, timestamp: Date.now(), retryCount: 0 });
185
365
  });
186
366
 
187
- 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) {
188
375
  await this.flushQueue();
189
376
  } else if (!this.flushTimeoutId) {
190
377
  this.flushTimeoutId = setTimeout(
@@ -194,142 +381,238 @@ export class Chirpier {
194
381
  }
195
382
  }
196
383
 
197
- /**
198
- * Flushes the event queue by sending all events to the API.
199
- */
200
384
  private async flushQueue(): Promise<void> {
201
- // Acquire the flush lock
202
385
  await this.flushLock.acquire("flush", async () => {
203
- let eventsToSend: QueuedEvent[] = [];
386
+ let logsToSend: QueuedLog[] = [];
204
387
 
205
- // Extract events from the queue under the queue lock
206
- await this.queueLock.acquire("eventQueue", async () => {
207
- if (this.eventQueue.length > 0) {
208
- eventsToSend = [...this.eventQueue];
209
- this.eventQueue = [];
388
+ await this.queueLock.acquire("logQueue", async () => {
389
+ if (this.logQueue.length > 0) {
390
+ logsToSend = [...this.logQueue];
391
+ this.logQueue = [];
210
392
  }
211
393
  });
212
394
 
213
- if (eventsToSend.length === 0) {
395
+ if (logsToSend.length === 0) {
214
396
  return;
215
397
  }
216
398
 
217
399
  try {
218
- // Clear any pending flush timeout
219
400
  if (this.flushTimeoutId) {
220
401
  clearTimeout(this.flushTimeoutId);
221
402
  this.flushTimeoutId = null;
222
403
  }
223
404
 
224
- // Attempt to send events
225
- await this.sendEvents(eventsToSend.map((qe) => qe.event));
405
+ await this.sendLogs(logsToSend.map((queuedLog) => queuedLog.log));
226
406
 
227
407
  if (this.logLevel >= LogLevel.Info) {
228
- console.info(`Successfully sent ${eventsToSend.length} events`);
408
+ console.info(`Successfully sent ${logsToSend.length} logs`);
229
409
  }
230
410
  } catch (error) {
231
- // Log failure
232
411
  if (this.logLevel >= LogLevel.Error) {
233
- console.error("Failed to send events:", error);
412
+ console.error("Failed to send logs:", error);
234
413
  }
235
414
 
236
- // Requeue failed events with retry count checks
237
- const retryableEvents: QueuedEvent[] = [];
238
- for (const queuedEvent of eventsToSend) {
239
- if (queuedEvent.retryCount >= this.retries) {
415
+ const retryableLogs: QueuedLog[] = [];
416
+ for (const queuedLog of logsToSend) {
417
+ if (queuedLog.retryCount >= this.retries) {
240
418
  if (this.logLevel >= LogLevel.Error) {
241
419
  console.error(
242
- `Dropping event after ${this.retries} retries:`,
243
- queuedEvent.event
420
+ `Dropping log after ${this.retries} retries:`,
421
+ queuedLog.log
244
422
  );
245
423
  }
246
- continue; // Skip adding this event back to the queue
424
+ continue;
247
425
  }
248
426
 
249
- // Increment retry count and add back to the queue
250
- queuedEvent.retryCount++;
251
- retryableEvents.push(queuedEvent);
427
+ queuedLog.retryCount++;
428
+ retryableLogs.push(queuedLog);
252
429
  }
253
430
 
254
- // Requeue remaining retryable events
255
- await this.queueLock.acquire("eventQueue", async () => {
256
- this.eventQueue = [...retryableEvents, ...this.eventQueue];
431
+ await this.queueLock.acquire("logQueue", async () => {
432
+ this.logQueue = [...retryableLogs, ...this.logQueue];
257
433
  });
258
434
  }
259
435
  });
260
436
  }
261
437
 
262
- /**
263
- * Sends multiple events to the API in a batch.
264
- * @param events - The array of events to send.
265
- */
266
- private async sendEvents(events: Event[]): Promise<void> {
267
- await this.axiosInstance.post(this.apiEndpoint, events);
438
+ private async sendLogs(logs: Log[]): Promise<void> {
439
+ await this.axiosInstance.post(this.apiEndpoint, logs);
268
440
  }
269
441
 
270
- // Stop the timeout and uninitialize the Chirpier instance
271
- public static async stop(): Promise<void> {
272
- if (!Chirpier.instance) {
273
- return;
274
- }
275
- if (Chirpier.instance.flushTimeoutId) {
276
- clearTimeout(Chirpier.instance.flushTimeoutId);
277
- 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;
278
450
  }
279
- // Flush any remaining events in the queue
280
- await Chirpier.instance.flushQueue();
281
- // Uninitialize the Chirpier instance
282
- 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;
283
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
+
284
549
  }
285
550
 
286
- /**
287
- * Decodes a base64url encoded string.
288
- * @param str - The base64url encoded string to decode.
289
- * @returns The decoded string.
290
- */
291
- function base64UrlDecode(str: string): string {
292
- // Replace '-' with '+' and '_' with '/'
293
- let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
294
- // Pad the base64 string
295
- const padding = base64.length % 4;
296
- if (padding !== 0) {
297
- base64 += "=".repeat(4 - padding);
298
- }
299
- return Base64.decode(base64);
551
+ let instance: Client | null = null;
552
+
553
+ export function createClient(config: Config = {}): Client {
554
+ return new Client(config);
300
555
  }
301
556
 
302
- /**
303
- * Validates if the provided token is a valid JWT.
304
- * @param token - The token to validate.
305
- * @returns True if valid, false otherwise.
306
- */
307
- function isValidJWT(token: string): boolean {
308
- const parts = token.split(".");
309
- if (parts.length !== 3) {
310
- 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;
311
568
  }
569
+
312
570
  try {
313
- const header = JSON.parse(base64UrlDecode(parts[0]));
314
- const payload = JSON.parse(base64UrlDecode(parts[1]));
315
- return typeof header === "object" && typeof payload === "object";
316
- } catch (error) {
317
- console.error("Failed to validate JWT:", error);
318
- 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();
319
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();
320
598
  }
321
599
 
322
- /**
323
- * Initializes the Chirpier SDK.
324
- * @param options - Configuration options for the SDK.
325
- */
326
- export function initialize(options: Options): void {
327
- if (!isValidJWT(options.key)) {
328
- 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;
329
612
  }
330
613
 
331
614
  try {
332
- Chirpier.getInstance(options);
615
+ instance = new Client({ ...options, key: resolvedKey });
333
616
  } catch (error) {
334
617
  if (error instanceof ChirpierError) {
335
618
  if (options.logLevel && options.logLevel >= LogLevel.Error) {
@@ -347,19 +630,33 @@ export function initialize(options: Options): void {
347
630
  }
348
631
  }
349
632
 
350
- /**
351
- * Monitors an event using the Chirpier SDK.
352
- * @param event - The event to monitor.
353
- */
354
- export function monitor(event: Event): void {
355
- 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> {
356
654
  if (!instance) {
357
655
  throw new ChirpierError(
358
- "Chirpier SDK is not initialized. Please call initialize() first."
656
+ "Chirpier SDK is not initialized. Please call initialize() first.",
657
+ "NOT_INITIALIZED"
359
658
  );
360
659
  }
361
660
 
362
- instance.monitor(event).catch((error) => {
363
- console.error("Error in monitor function:", error);
364
- });
661
+ await instance.flush();
365
662
  }