@chirpier/chirpier-js 0.1.6 → 0.2.1

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