@marginfront/sdk 0.0.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/dist/index.mjs ADDED
@@ -0,0 +1,1311 @@
1
+ // src/utils/http.ts
2
+ import axios from "axios";
3
+
4
+ // src/errors.ts
5
+ var MarginFrontError = class _MarginFrontError extends Error {
6
+ constructor(message, statusCode = 500, code = "MARGINFRONT_ERROR", details, metadata = {}) {
7
+ super(message);
8
+ this.name = "MarginFrontError";
9
+ this.statusCode = statusCode;
10
+ this.code = code;
11
+ this.details = details;
12
+ this.metadata = metadata;
13
+ this.requestId = metadata.requestId;
14
+ Object.setPrototypeOf(this, _MarginFrontError.prototype);
15
+ if (Error.captureStackTrace) {
16
+ Error.captureStackTrace(this, this.constructor);
17
+ }
18
+ }
19
+ };
20
+ var AuthenticationError = class _AuthenticationError extends MarginFrontError {
21
+ constructor(message = "Invalid or missing API key", metadata = {}) {
22
+ super(message, 401, "AUTHENTICATION_ERROR", void 0, metadata);
23
+ this.name = "AuthenticationError";
24
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
25
+ }
26
+ };
27
+ var AuthorizationError = class _AuthorizationError extends MarginFrontError {
28
+ constructor(message = "You do not have permission to perform this action", metadata = {}) {
29
+ super(message, 403, "AUTHORIZATION_ERROR", void 0, metadata);
30
+ this.name = "AuthorizationError";
31
+ Object.setPrototypeOf(this, _AuthorizationError.prototype);
32
+ }
33
+ };
34
+ var NotFoundError = class _NotFoundError extends MarginFrontError {
35
+ constructor(message = "Resource not found", resourceType, resourceId, metadata = {}) {
36
+ super(message, 404, "NOT_FOUND_ERROR", void 0, metadata);
37
+ this.name = "NotFoundError";
38
+ this.resourceType = resourceType;
39
+ this.resourceId = resourceId;
40
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
41
+ }
42
+ };
43
+ var ValidationError = class _ValidationError extends MarginFrontError {
44
+ constructor(message = "Validation failed", validationErrors, metadata = {}) {
45
+ super(message, 400, "VALIDATION_ERROR", validationErrors, metadata);
46
+ this.name = "ValidationError";
47
+ this.validationErrors = validationErrors;
48
+ Object.setPrototypeOf(this, _ValidationError.prototype);
49
+ }
50
+ };
51
+ var RateLimitError = class _RateLimitError extends MarginFrontError {
52
+ constructor(message = "Rate limit exceeded", retryAfter, metadata = {}) {
53
+ super(message, 429, "RATE_LIMIT_ERROR", void 0, metadata);
54
+ this.name = "RateLimitError";
55
+ this.retryAfter = retryAfter;
56
+ Object.setPrototypeOf(this, _RateLimitError.prototype);
57
+ }
58
+ };
59
+ var ConflictError = class _ConflictError extends MarginFrontError {
60
+ constructor(message = "Resource conflict", metadata = {}) {
61
+ super(message, 409, "CONFLICT_ERROR", void 0, metadata);
62
+ this.name = "ConflictError";
63
+ Object.setPrototypeOf(this, _ConflictError.prototype);
64
+ }
65
+ };
66
+ var NetworkError = class _NetworkError extends MarginFrontError {
67
+ constructor(message = "Network error occurred", originalError, metadata = {}) {
68
+ super(message, 0, "NETWORK_ERROR", void 0, metadata);
69
+ this.name = "NetworkError";
70
+ this.originalError = originalError;
71
+ Object.setPrototypeOf(this, _NetworkError.prototype);
72
+ }
73
+ };
74
+ var TimeoutError = class _TimeoutError extends MarginFrontError {
75
+ constructor(message = "Request timed out", timeoutMs, metadata = {}) {
76
+ super(message, 0, "TIMEOUT_ERROR", void 0, metadata);
77
+ this.name = "TimeoutError";
78
+ this.timeoutMs = timeoutMs;
79
+ Object.setPrototypeOf(this, _TimeoutError.prototype);
80
+ }
81
+ };
82
+ var InternalError = class _InternalError extends MarginFrontError {
83
+ constructor(message = "Internal server error", metadata = {}) {
84
+ super(message, 500, "INTERNAL_ERROR", void 0, metadata);
85
+ this.name = "InternalError";
86
+ Object.setPrototypeOf(this, _InternalError.prototype);
87
+ }
88
+ };
89
+ var InitializationError = class _InitializationError extends MarginFrontError {
90
+ constructor(message = "Failed to initialize the SDK", metadata = {}) {
91
+ super(message, 0, "INITIALIZATION_ERROR", void 0, metadata);
92
+ this.name = "InitializationError";
93
+ Object.setPrototypeOf(this, _InitializationError.prototype);
94
+ }
95
+ };
96
+ var ApiError = class _ApiError extends MarginFrontError {
97
+ constructor(message = "API request failed", statusCode, errorCode, metadata = {}) {
98
+ super(message, statusCode || 500, errorCode || "API_ERROR", void 0, metadata);
99
+ this.name = "ApiError";
100
+ this.errorCode = errorCode;
101
+ Object.setPrototypeOf(this, _ApiError.prototype);
102
+ }
103
+ };
104
+ function createErrorFromResponse(statusCode, message, details, metadata = {}) {
105
+ switch (statusCode) {
106
+ case 400:
107
+ return new ValidationError(message, details, metadata);
108
+ case 401:
109
+ return new AuthenticationError(message, metadata);
110
+ case 403:
111
+ return new AuthorizationError(message, metadata);
112
+ case 404:
113
+ return new NotFoundError(message, void 0, void 0, metadata);
114
+ case 409:
115
+ return new ConflictError(message, metadata);
116
+ case 422:
117
+ return new ValidationError(message, details, metadata);
118
+ case 429:
119
+ const retryAfter = metadata.retryAfter;
120
+ return new RateLimitError(message, retryAfter, metadata);
121
+ case 500:
122
+ case 502:
123
+ case 503:
124
+ case 504:
125
+ return new InternalError(message, metadata);
126
+ default:
127
+ return new ApiError(message, statusCode, "API_ERROR", metadata);
128
+ }
129
+ }
130
+ function parseApiError(error) {
131
+ const err = error;
132
+ const message = err.message || "Unknown error";
133
+ const statusCode = err.response?.status;
134
+ const responseData = err.response?.data || {};
135
+ const requestId = err.config?.headers?.["X-Request-ID"] || "unknown";
136
+ const metadata = {
137
+ requestId,
138
+ url: err.config?.url,
139
+ method: err.config?.method,
140
+ responseData,
141
+ retryAfter: err.response?.headers?.["retry-after"] ? parseInt(err.response.headers["retry-after"], 10) : void 0
142
+ };
143
+ if (!statusCode) {
144
+ if (err.code === "ECONNABORTED") {
145
+ return new TimeoutError(message, void 0, metadata);
146
+ }
147
+ return new NetworkError(message, err, metadata);
148
+ }
149
+ return createErrorFromResponse(
150
+ statusCode,
151
+ responseData.message || responseData.error || message,
152
+ responseData.errors,
153
+ metadata
154
+ );
155
+ }
156
+
157
+ // src/utils/http.ts
158
+ var DEFAULT_BASE_URL = "https://api-qa.costingly.com/v1";
159
+ var DEFAULT_TIMEOUT = 3e4;
160
+ var DEFAULT_RETRIES = 0;
161
+ var DEFAULT_RETRY_DELAY = 300;
162
+ var Logger = class {
163
+ constructor(config) {
164
+ this.levelPriority = {
165
+ debug: 0,
166
+ info: 1,
167
+ warn: 2,
168
+ error: 3
169
+ };
170
+ this.defaultHandler = (level, message, data) => {
171
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
172
+ const logMessage = `[${timestamp}] [MarginFront] [${level.toUpperCase()}] ${message}`;
173
+ const logFn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
174
+ if (data) {
175
+ logFn(logMessage, data);
176
+ } else {
177
+ logFn(logMessage);
178
+ }
179
+ };
180
+ this.enabled = config.logging?.enabled ?? config.debug ?? false;
181
+ this.minLevel = config.logging?.level ?? "info";
182
+ this.handler = config.logging?.handler ?? this.defaultHandler;
183
+ }
184
+ shouldLog(level) {
185
+ return this.enabled && this.levelPriority[level] >= this.levelPriority[this.minLevel];
186
+ }
187
+ debug(message, data) {
188
+ if (this.shouldLog("debug")) this.handler("debug", message, data);
189
+ }
190
+ info(message, data) {
191
+ if (this.shouldLog("info")) this.handler("info", message, data);
192
+ }
193
+ warn(message, data) {
194
+ if (this.shouldLog("warn")) this.handler("warn", message, data);
195
+ }
196
+ error(message, data) {
197
+ if (this.shouldLog("error")) this.handler("error", message, data);
198
+ }
199
+ };
200
+ var Telemetry = class {
201
+ constructor(config) {
202
+ this.activeRequests = /* @__PURE__ */ new Map();
203
+ this.requestCount = 0;
204
+ this.successCount = 0;
205
+ this.errorCount = 0;
206
+ this.totalDuration = 0;
207
+ this.errorTypes = /* @__PURE__ */ new Map();
208
+ this.enabled = config.telemetry?.enabled ?? false;
209
+ this.sampleRate = config.telemetry?.sampleRate ?? 1;
210
+ this.handler = config.telemetry?.handler ?? (() => {
211
+ });
212
+ }
213
+ shouldCollect() {
214
+ return this.enabled && Math.random() <= this.sampleRate;
215
+ }
216
+ generateRequestId() {
217
+ return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
218
+ }
219
+ normalizePath(path) {
220
+ if (!path) return "";
221
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
222
+ return normalizedPath.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "/:id").replace(/\/\d+(?=\/|$)/g, "/:id");
223
+ }
224
+ startRequest(method, path, requestId) {
225
+ const shouldTrack = this.shouldCollect();
226
+ const id = requestId || this.generateRequestId();
227
+ if (shouldTrack) {
228
+ const metrics = {
229
+ requestId: id,
230
+ method: method.toUpperCase(),
231
+ path: this.normalizePath(path),
232
+ startTime: Date.now()
233
+ };
234
+ this.activeRequests.set(id, metrics);
235
+ }
236
+ return { requestId: id, isTracked: shouldTrack };
237
+ }
238
+ endRequest(requestId, statusCode, error, retryCount) {
239
+ const metrics = this.activeRequests.get(requestId);
240
+ if (!metrics) return;
241
+ metrics.endTime = Date.now();
242
+ metrics.duration = metrics.endTime - metrics.startTime;
243
+ metrics.statusCode = statusCode;
244
+ metrics.success = !error && (statusCode ? statusCode < 400 : true);
245
+ metrics.retryCount = retryCount || 0;
246
+ if (error) {
247
+ metrics.errorMessage = error.message;
248
+ metrics.errorType = error.name || "UnknownError";
249
+ const currentCount = this.errorTypes.get(metrics.errorType) || 0;
250
+ this.errorTypes.set(metrics.errorType, currentCount + 1);
251
+ this.errorCount++;
252
+ } else {
253
+ this.successCount++;
254
+ }
255
+ this.requestCount++;
256
+ this.totalDuration += metrics.duration;
257
+ this.handler(metrics);
258
+ this.activeRequests.delete(requestId);
259
+ }
260
+ getStats() {
261
+ return {
262
+ requestCount: this.requestCount,
263
+ successCount: this.successCount,
264
+ errorCount: this.errorCount,
265
+ successRate: this.requestCount > 0 ? this.successCount / this.requestCount : 1,
266
+ averageDuration: this.requestCount > 0 ? this.totalDuration / this.requestCount : 0,
267
+ errorBreakdown: Object.fromEntries(this.errorTypes)
268
+ };
269
+ }
270
+ resetStats() {
271
+ this.requestCount = 0;
272
+ this.successCount = 0;
273
+ this.errorCount = 0;
274
+ this.totalDuration = 0;
275
+ this.errorTypes.clear();
276
+ }
277
+ };
278
+ function calculateBackoff(retryCount, initialDelay) {
279
+ return initialDelay * Math.pow(2, retryCount);
280
+ }
281
+ function sleep(ms) {
282
+ return new Promise((resolve) => setTimeout(resolve, ms));
283
+ }
284
+ function isRetryable(error) {
285
+ if (error.response) {
286
+ const status = error.response.status;
287
+ return status === 429 || status >= 500 && status < 600;
288
+ }
289
+ return !error.response;
290
+ }
291
+ var HttpClient = class {
292
+ constructor(config) {
293
+ this.logger = new Logger(config);
294
+ this.telemetry = new Telemetry(config);
295
+ this.retries = config.retries ?? DEFAULT_RETRIES;
296
+ this.retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY;
297
+ const baseURL = config.baseUrl || config.baseURL || DEFAULT_BASE_URL;
298
+ const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
299
+ const headers = {
300
+ "Content-Type": "application/json",
301
+ Accept: "application/json",
302
+ "x-api-key": config.apiKey,
303
+ ...config.headers
304
+ };
305
+ if (!isBrowser) {
306
+ headers["User-Agent"] = "marginfront-node/1.0.0";
307
+ }
308
+ this.client = axios.create({
309
+ baseURL,
310
+ timeout: config.timeout || DEFAULT_TIMEOUT,
311
+ headers
312
+ });
313
+ this.client.interceptors.request.use(
314
+ (requestConfig) => {
315
+ const customConfig = requestConfig;
316
+ customConfig.metadata = customConfig.metadata || {};
317
+ const { requestId, isTracked } = this.telemetry.startRequest(
318
+ requestConfig.method || "GET",
319
+ requestConfig.url || "",
320
+ customConfig.metadata.requestId
321
+ );
322
+ requestConfig.headers = requestConfig.headers || {};
323
+ requestConfig.headers["X-Request-ID"] = requestId;
324
+ customConfig.metadata.requestId = requestId;
325
+ customConfig.metadata.telemetryTracked = isTracked;
326
+ this.logger.debug(`Request: ${requestConfig.method?.toUpperCase()} ${requestConfig.url}`, {
327
+ requestId
328
+ });
329
+ return requestConfig;
330
+ },
331
+ (error) => Promise.reject(error)
332
+ );
333
+ this.client.interceptors.response.use(
334
+ (response) => {
335
+ const customConfig = response.config;
336
+ const requestId = customConfig.metadata?.requestId;
337
+ const isTracked = customConfig.metadata?.telemetryTracked;
338
+ if (isTracked && requestId) {
339
+ this.telemetry.endRequest(requestId, response.status);
340
+ }
341
+ this.logger.debug(`Response: ${response.status}`, { requestId });
342
+ return response;
343
+ },
344
+ (error) => {
345
+ const customConfig = error.config;
346
+ const requestId = customConfig?.metadata?.requestId;
347
+ const isTracked = customConfig?.metadata?.telemetryTracked;
348
+ const retryCount = customConfig?.metadata?.retryCount || 0;
349
+ if (isTracked && requestId) {
350
+ this.telemetry.endRequest(requestId, error.response?.status, error, retryCount);
351
+ }
352
+ this.logger.error(`Error: ${error.message}`, {
353
+ requestId,
354
+ status: error.response?.status
355
+ });
356
+ return Promise.reject(error);
357
+ }
358
+ );
359
+ }
360
+ /**
361
+ * Make a request with retry logic
362
+ */
363
+ async request(config) {
364
+ let lastError;
365
+ config.metadata = config.metadata || {};
366
+ for (let retry = 0; retry <= this.retries; retry++) {
367
+ try {
368
+ config.metadata.retryCount = retry;
369
+ if (retry > 0) {
370
+ this.logger.info(`Retry attempt ${retry}/${this.retries}`, {
371
+ requestId: config.metadata.requestId
372
+ });
373
+ }
374
+ const response = await this.client.request(config);
375
+ return response.data;
376
+ } catch (error) {
377
+ lastError = error;
378
+ if (retry < this.retries && isRetryable(lastError)) {
379
+ const retryAfterHeader = lastError.response?.headers?.["retry-after"];
380
+ const backoffTime = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : calculateBackoff(retry, this.retryDelay);
381
+ this.logger.debug(`Retrying in ${backoffTime}ms...`);
382
+ await sleep(backoffTime);
383
+ continue;
384
+ }
385
+ break;
386
+ }
387
+ }
388
+ throw parseApiError(lastError);
389
+ }
390
+ /**
391
+ * Make a GET request
392
+ */
393
+ async get(path, params) {
394
+ const config = {
395
+ method: "GET",
396
+ url: path
397
+ };
398
+ if (params) {
399
+ config.params = Object.fromEntries(
400
+ Object.entries(params).filter(([, v]) => v !== void 0 && v !== null)
401
+ );
402
+ }
403
+ return this.request(config);
404
+ }
405
+ /**
406
+ * Make a POST request
407
+ */
408
+ async post(path, data) {
409
+ return this.request({
410
+ method: "POST",
411
+ url: path,
412
+ data
413
+ });
414
+ }
415
+ /**
416
+ * Make a PUT request
417
+ */
418
+ async put(path, data) {
419
+ return this.request({
420
+ method: "PUT",
421
+ url: path,
422
+ data
423
+ });
424
+ }
425
+ /**
426
+ * Make a PATCH request
427
+ */
428
+ async patch(path, data) {
429
+ return this.request({
430
+ method: "PATCH",
431
+ url: path,
432
+ data
433
+ });
434
+ }
435
+ /**
436
+ * Make a DELETE request
437
+ */
438
+ async delete(path) {
439
+ return this.request({
440
+ method: "DELETE",
441
+ url: path
442
+ });
443
+ }
444
+ /**
445
+ * Get telemetry statistics
446
+ */
447
+ getTelemetryStats() {
448
+ return this.telemetry.getStats();
449
+ }
450
+ /**
451
+ * Reset telemetry statistics
452
+ */
453
+ resetTelemetryStats() {
454
+ this.telemetry.resetStats();
455
+ }
456
+ };
457
+
458
+ // src/utils/validation.ts
459
+ function validateRequired(value, fieldName) {
460
+ if (value === void 0 || value === null || value === "") {
461
+ throw new ValidationError(`${fieldName} is required`);
462
+ }
463
+ }
464
+ function validateNonNegative(value, fieldName) {
465
+ if (typeof value !== "number" || isNaN(value)) {
466
+ throw new ValidationError(`${fieldName} must be a number`);
467
+ }
468
+ if (value < 0) {
469
+ throw new ValidationError(`${fieldName} must be non-negative`);
470
+ }
471
+ }
472
+ function validateDateString(value, fieldName) {
473
+ const date = value instanceof Date ? value : new Date(value);
474
+ if (isNaN(date.getTime())) {
475
+ throw new ValidationError(`${fieldName} must be a valid ISO date string`);
476
+ }
477
+ }
478
+ function validateUsageRecord(record) {
479
+ validateRequired(record.customerExternalId, "customerExternalId");
480
+ validateRequired(record.agentId, "agentId");
481
+ if (!record.signalName) {
482
+ throw new ValidationError("signalName is required");
483
+ }
484
+ if (record.quantity !== void 0 && record.quantity !== null) {
485
+ validateNonNegative(record.quantity, "quantity");
486
+ }
487
+ if (record.usageDate !== void 0) {
488
+ validateDateString(record.usageDate, "usageDate");
489
+ }
490
+ }
491
+ function validateUsageRecords(records) {
492
+ if (!Array.isArray(records)) {
493
+ throw new ValidationError("records must be an array");
494
+ }
495
+ if (records.length === 0) {
496
+ throw new ValidationError("records array cannot be empty");
497
+ }
498
+ records.forEach((record, index) => {
499
+ try {
500
+ validateUsageRecord(record);
501
+ } catch (error) {
502
+ if (error instanceof ValidationError) {
503
+ throw new ValidationError(`Invalid record at index ${index}: ${error.message}`);
504
+ }
505
+ throw error;
506
+ }
507
+ });
508
+ }
509
+ var KEY_PREFIXES = {
510
+ secret: "mf_sk_",
511
+ publishable: "mf_pk_"
512
+ };
513
+ function parseApiKey(apiKey) {
514
+ if (!apiKey || typeof apiKey !== "string") {
515
+ return null;
516
+ }
517
+ if (apiKey.startsWith(KEY_PREFIXES.secret)) {
518
+ return { type: "secret" };
519
+ }
520
+ if (apiKey.startsWith(KEY_PREFIXES.publishable)) {
521
+ return { type: "publishable" };
522
+ }
523
+ return null;
524
+ }
525
+ function isSecretKey(apiKey) {
526
+ const parsed = parseApiKey(apiKey);
527
+ return parsed !== null && parsed.type === "secret";
528
+ }
529
+ function isPublishableKey(apiKey) {
530
+ const parsed = parseApiKey(apiKey);
531
+ return parsed !== null && parsed.type === "publishable";
532
+ }
533
+ function getMinKeyLength(prefix) {
534
+ return prefix.length + 16;
535
+ }
536
+ function validateApiKey(apiKey) {
537
+ if (!apiKey || typeof apiKey !== "string") {
538
+ throw new ValidationError("API key is required");
539
+ }
540
+ const parsed = parseApiKey(apiKey);
541
+ if (!parsed) {
542
+ throw new ValidationError(
543
+ "Invalid API key format. Expected mf_sk_* or mf_pk_*"
544
+ );
545
+ }
546
+ const prefix = KEY_PREFIXES[parsed.type];
547
+ const minLength = getMinKeyLength(prefix);
548
+ if (apiKey.length < minLength) {
549
+ throw new ValidationError("Invalid API key format. Key is too short");
550
+ }
551
+ }
552
+
553
+ // src/resources/usage.ts
554
+ var UsageResource = class {
555
+ constructor(http) {
556
+ this.http = http;
557
+ }
558
+ /**
559
+ * Track events for a customer - supports both single record and batch operations
560
+ *
561
+ * @param params - Event tracking parameters (single record or batch)
562
+ * @returns Tracked event data
563
+ *
564
+ * @example
565
+ * ```typescript
566
+ * // Single event
567
+ * await client.usage.trackEvent({
568
+ * agentId: 'agent_123',
569
+ * customerExternalId: 'customer_456',
570
+ * signalName: 'api_call',
571
+ * quantity: 1
572
+ * });
573
+ *
574
+ * // Batch tracking
575
+ * await client.usage.trackEvent({
576
+ * records: [
577
+ * { customerExternalId: 'cust_1', agentId: 'agent_123', signalName: 'api_call', quantity: 10 },
578
+ * { customerExternalId: 'cust_2', agentId: 'agent_123', signalName: 'storage', quantity: 100 }
579
+ * ]
580
+ * });
581
+ * ```
582
+ */
583
+ async trackEvent(params) {
584
+ if ("records" in params) {
585
+ const records = params.records.map((record2) => this.normalizeRecord(record2));
586
+ validateUsageRecords(records);
587
+ const response2 = await this.http.post("/sdk/usage/record", {
588
+ records
589
+ });
590
+ return this.convertToBatchResponse(response2);
591
+ }
592
+ const record = this.normalizeRecord(params);
593
+ validateUsageRecord(record);
594
+ const response = await this.http.post("/sdk/usage/record", {
595
+ records: [record]
596
+ });
597
+ if (response.results.success.length === 1) {
598
+ const successResult = response.results.success[0];
599
+ return {
600
+ id: successResult.eventId,
601
+ success: true,
602
+ eventId: successResult.eventId,
603
+ rawEventId: successResult.rawEventId,
604
+ timestamp: successResult.timestamp
605
+ };
606
+ }
607
+ return this.convertToBatchResponse(response);
608
+ }
609
+ /**
610
+ * Record a single usage event
611
+ *
612
+ * @param record - The usage record to track
613
+ * @returns The response containing processing results
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * const result = await client.usage.record({
618
+ * customerExternalId: 'cust_123',
619
+ * agentId: 'agent_abc',
620
+ * signalName: 'api_call',
621
+ * quantity: 1,
622
+ * metadata: { model: 'gpt-4' }
623
+ * });
624
+ * ```
625
+ */
626
+ async record(record) {
627
+ const normalizedRecord = this.normalizeRecord(record);
628
+ validateUsageRecord(normalizedRecord);
629
+ return this.http.post("/sdk/usage/record", {
630
+ records: [normalizedRecord]
631
+ });
632
+ }
633
+ /**
634
+ * Record multiple usage events in a batch
635
+ *
636
+ * @param records - Array of usage records to track
637
+ * @returns The response containing processing results for all records
638
+ *
639
+ * @example
640
+ * ```typescript
641
+ * const result = await client.usage.recordBatch([
642
+ * { customerExternalId: 'cust_123', agentId: 'agent_abc', signalName: 'api_call', quantity: 5 },
643
+ * { customerExternalId: 'cust_456', agentId: 'agent_abc', signalName: 'api_call', quantity: 3 },
644
+ * ]);
645
+ * console.log(`Processed ${result.successful} of ${result.processed} records`);
646
+ * ```
647
+ */
648
+ async recordBatch(records) {
649
+ const normalizedRecords = records.map((record) => this.normalizeRecord(record));
650
+ validateUsageRecords(normalizedRecords);
651
+ return this.http.post("/sdk/usage/record", {
652
+ records: normalizedRecords
653
+ });
654
+ }
655
+ /**
656
+ * Normalize a usage record
657
+ */
658
+ normalizeRecord(record) {
659
+ const normalized = { ...record };
660
+ if (normalized.quantity === void 0) {
661
+ normalized.quantity = 1;
662
+ }
663
+ if (normalized.usageDate instanceof Date) {
664
+ normalized.usageDate = normalized.usageDate.toISOString();
665
+ }
666
+ return normalized;
667
+ }
668
+ /**
669
+ * Convert native response to BatchEventResponse format
670
+ */
671
+ convertToBatchResponse(response) {
672
+ return {
673
+ success: response.failed === 0,
674
+ totalRecords: response.processed,
675
+ successCount: response.successful,
676
+ failureCount: response.failed,
677
+ results: [
678
+ ...response.results.success.map((result) => ({
679
+ success: true,
680
+ responseData: {
681
+ id: result.eventId,
682
+ success: true,
683
+ eventId: result.eventId,
684
+ rawEventId: result.rawEventId,
685
+ timestamp: result.timestamp
686
+ }
687
+ })),
688
+ ...response.results.failed.map((result) => ({
689
+ success: false,
690
+ error: result.error,
691
+ originalData: result.record
692
+ }))
693
+ ]
694
+ };
695
+ }
696
+ };
697
+
698
+ // src/resources/customers.ts
699
+ var CustomersResource = class {
700
+ constructor(http) {
701
+ this.http = http;
702
+ }
703
+ /**
704
+ * Create a new customer
705
+ *
706
+ * @param params - Customer creation parameters
707
+ * @returns The created customer
708
+ *
709
+ * @example
710
+ * ```typescript
711
+ * const customer = await client.customers.create({
712
+ * name: 'Acme Corp',
713
+ * externalId: 'acme_123',
714
+ * email: 'billing@acme.com',
715
+ * agentId: 'agent_abc' // auto-creates subscription
716
+ * });
717
+ * ```
718
+ */
719
+ async create(params) {
720
+ validateRequired(params.name, "name");
721
+ return this.http.post("/sdk/customers", params);
722
+ }
723
+ /**
724
+ * Get a single customer by ID
725
+ *
726
+ * @param id - The customer's ID
727
+ * @returns The customer with subscriptions
728
+ *
729
+ * @example
730
+ * ```typescript
731
+ * const customer = await client.customers.get('cust_123');
732
+ * console.log(customer.name);
733
+ * ```
734
+ */
735
+ async get(id) {
736
+ validateRequired(id, "id");
737
+ return this.http.get(`/sdk/customers/${id}`);
738
+ }
739
+ /**
740
+ * Update an existing customer
741
+ *
742
+ * @param id - The customer's ID
743
+ * @param params - Fields to update
744
+ * @returns The updated customer
745
+ *
746
+ * @example
747
+ * ```typescript
748
+ * const customer = await client.customers.update('cust_123', {
749
+ * name: 'New Company Name',
750
+ * email: 'new-email@company.com'
751
+ * });
752
+ * ```
753
+ */
754
+ async update(id, params) {
755
+ validateRequired(id, "id");
756
+ return this.http.patch(`/sdk/customers/${id}`, params);
757
+ }
758
+ /**
759
+ * Delete a customer
760
+ *
761
+ * @param id - The customer's ID
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * await client.customers.delete('cust_123');
766
+ * ```
767
+ */
768
+ async delete(id) {
769
+ validateRequired(id, "id");
770
+ await this.http.delete(`/sdk/customers/${id}`);
771
+ }
772
+ /**
773
+ * List customers with pagination and filtering
774
+ *
775
+ * @param params - List parameters
776
+ * @returns Paginated list of customers
777
+ *
778
+ * @example
779
+ * ```typescript
780
+ * const { data, totalResults, hasMore } = await client.customers.list({
781
+ * limit: 10,
782
+ * page: 1
783
+ * });
784
+ * ```
785
+ */
786
+ async list(params) {
787
+ const response = await this.http.get("/sdk/customers", params);
788
+ if (Array.isArray(response)) {
789
+ return {
790
+ data: response,
791
+ totalResults: response.length,
792
+ page: params?.page || 1,
793
+ limit: params?.limit || response.length,
794
+ hasMore: false
795
+ };
796
+ }
797
+ return response;
798
+ }
799
+ };
800
+
801
+ // src/resources/invoices.ts
802
+ var InvoicesResource = class {
803
+ constructor(http) {
804
+ this.http = http;
805
+ }
806
+ /**
807
+ * List invoices with optional filters
808
+ *
809
+ * @param params - Optional filter parameters
810
+ * @returns Paginated list of invoices
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * const { invoices, totalResults } = await client.invoices.list({
815
+ * customerId: 'cust_123',
816
+ * status: 'pending',
817
+ * page: 1,
818
+ * limit: 20
819
+ * });
820
+ * ```
821
+ */
822
+ async list(params) {
823
+ return this.http.get("/sdk/invoices", params);
824
+ }
825
+ /**
826
+ * Get a single invoice by ID
827
+ *
828
+ * @param invoiceId - The invoice's ID
829
+ * @returns The invoice with full details including line items and payments
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * const invoice = await client.invoices.get('inv_abc');
834
+ * console.log(`Invoice ${invoice.invoiceNumber}: ${invoice.totalAmount}`);
835
+ * ```
836
+ */
837
+ async get(invoiceId) {
838
+ validateRequired(invoiceId, "invoiceId");
839
+ return this.http.get(`/sdk/invoices/${invoiceId}`);
840
+ }
841
+ };
842
+
843
+ // src/resources/analytics.ts
844
+ var AnalyticsResource = class {
845
+ constructor(http) {
846
+ this.http = http;
847
+ }
848
+ /**
849
+ * Get usage analytics for a date range
850
+ *
851
+ * @param params - Analytics query parameters
852
+ * @returns Usage analytics with summary and time-series data
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * const analytics = await client.analytics.usage({
857
+ * startDate: '2024-01-01',
858
+ * endDate: '2024-01-31',
859
+ * groupBy: 'daily',
860
+ * customerId: 'cust_123'
861
+ * });
862
+ *
863
+ * console.log(`Total usage: ${analytics.summary.totalQuantity}`);
864
+ * console.log(`Total cost: $${analytics.summary.totalCost}`);
865
+ *
866
+ * // Time series data
867
+ * analytics.data.forEach(point => {
868
+ * console.log(`${point.date}: ${point.quantity} units, $${point.cost}`);
869
+ * });
870
+ * ```
871
+ */
872
+ async usage(params) {
873
+ validateRequired(params.startDate, "startDate");
874
+ validateRequired(params.endDate, "endDate");
875
+ validateDateString(params.startDate, "startDate");
876
+ validateDateString(params.endDate, "endDate");
877
+ return this.http.get("/sdk/analytics/usage", params);
878
+ }
879
+ };
880
+
881
+ // src/resources/subscriptions.ts
882
+ var SubscriptionsResource = class {
883
+ constructor(http) {
884
+ this.http = http;
885
+ }
886
+ /**
887
+ * List subscriptions with optional filters
888
+ *
889
+ * @param params - Optional filter parameters
890
+ * @returns Paginated list of subscriptions
891
+ *
892
+ * @example
893
+ * ```typescript
894
+ * const { subscriptions, totalResults } = await client.subscriptions.list({
895
+ * status: 'active',
896
+ * customerId: 'cust_123',
897
+ * page: 1,
898
+ * limit: 20
899
+ * });
900
+ * ```
901
+ */
902
+ async list(params) {
903
+ return this.http.get("/sdk/subscriptions", params);
904
+ }
905
+ /**
906
+ * Get a single subscription by ID
907
+ *
908
+ * @param subscriptionId - The subscription's ID
909
+ * @returns The subscription with full details including usage summary
910
+ *
911
+ * @example
912
+ * ```typescript
913
+ * const sub = await client.subscriptions.get('sub_abc');
914
+ * console.log(`Status: ${sub.status}`);
915
+ * console.log(`Usage this period: ${sub.usage.totalQuantity}`);
916
+ * ```
917
+ */
918
+ async get(subscriptionId) {
919
+ validateRequired(subscriptionId, "subscriptionId");
920
+ return this.http.get(`/sdk/subscriptions/${subscriptionId}`);
921
+ }
922
+ };
923
+
924
+ // src/resources/portal-sessions.ts
925
+ var PortalSessionsResource = class {
926
+ constructor(http, assertSecretKey) {
927
+ this.http = http;
928
+ this.assertSecretKey = assertSecretKey;
929
+ }
930
+ /**
931
+ * Create a new portal session
932
+ *
933
+ * Creates a short-lived portal session for a customer.
934
+ * The returned URL can be used to redirect the customer to their billing portal.
935
+ *
936
+ * @param params - Portal session parameters
937
+ * @returns Created portal session with URL and token
938
+ * @throws ValidationError if neither customerId nor customerExternalId is provided
939
+ * @throws AuthenticationError if using a publishable key
940
+ *
941
+ * @example
942
+ * ```typescript
943
+ * // By customer ID
944
+ * const session = await client.portalSessions.create({
945
+ * customerId: 'cust_123',
946
+ * returnUrl: 'https://myapp.com/account'
947
+ * });
948
+ *
949
+ * // By external ID
950
+ * const session = await client.portalSessions.create({
951
+ * customerExternalId: 'your_customer_id',
952
+ * features: ['invoices', 'usage']
953
+ * });
954
+ *
955
+ * console.log(session.url); // Redirect customer here
956
+ * ```
957
+ */
958
+ async create(params) {
959
+ this.assertSecretKey();
960
+ if (!params.customerId && !params.customerExternalId) {
961
+ throw new ValidationError(
962
+ "Either customerId or customerExternalId is required"
963
+ );
964
+ }
965
+ return this.http.post("/sdk/portal-sessions", params);
966
+ }
967
+ /**
968
+ * Get a portal session by ID
969
+ *
970
+ * @param sessionId - Portal session ID
971
+ * @returns Portal session details
972
+ *
973
+ * @example
974
+ * ```typescript
975
+ * const session = await client.portalSessions.get('ps_123');
976
+ * console.log(session.expiresAt);
977
+ * ```
978
+ */
979
+ async get(sessionId) {
980
+ this.assertSecretKey();
981
+ if (!sessionId) {
982
+ throw new ValidationError("sessionId is required");
983
+ }
984
+ return this.http.get(`/sdk/portal-sessions/${sessionId}`);
985
+ }
986
+ /**
987
+ * List portal sessions
988
+ *
989
+ * @param params - Optional filters
990
+ * @returns List of portal sessions
991
+ *
992
+ * @example
993
+ * ```typescript
994
+ * // List all active sessions
995
+ * const { data } = await client.portalSessions.list();
996
+ *
997
+ * // List sessions for a specific customer
998
+ * const { data } = await client.portalSessions.list({
999
+ * customerId: 'cust_123',
1000
+ * includeExpired: true
1001
+ * });
1002
+ * ```
1003
+ */
1004
+ async list(params) {
1005
+ this.assertSecretKey();
1006
+ return this.http.get("/sdk/portal-sessions", {
1007
+ customerId: params?.customerId,
1008
+ limit: params?.limit,
1009
+ includeExpired: params?.includeExpired
1010
+ });
1011
+ }
1012
+ /**
1013
+ * Revoke a portal session
1014
+ *
1015
+ * Immediately invalidates the session, preventing further access.
1016
+ *
1017
+ * @param sessionId - Portal session ID to revoke
1018
+ *
1019
+ * @example
1020
+ * ```typescript
1021
+ * await client.portalSessions.revoke('ps_123');
1022
+ * ```
1023
+ */
1024
+ async revoke(sessionId) {
1025
+ this.assertSecretKey();
1026
+ if (!sessionId) {
1027
+ throw new ValidationError("sessionId is required");
1028
+ }
1029
+ await this.http.delete(`/sdk/portal-sessions/${sessionId}`);
1030
+ }
1031
+ };
1032
+
1033
+ // src/client.ts
1034
+ var MarginFrontClient = class {
1035
+ /**
1036
+ * Create a new MarginFront client
1037
+ *
1038
+ * @param apiKeyOrConfig - Either an API key string or a configuration object
1039
+ * @param options - Optional client options (when first param is API key)
1040
+ *
1041
+ * @example
1042
+ * ```typescript
1043
+ * // Simple initialization
1044
+ * const client = new MarginFrontClient('mf_sk_your_secret_key');
1045
+ *
1046
+ * // With options
1047
+ * const client = new MarginFrontClient('mf_sk_your_secret_key', {
1048
+ * timeout: 10000,
1049
+ * retries: 3
1050
+ * });
1051
+ *
1052
+ * // With config object
1053
+ * const client = new MarginFrontClient({
1054
+ * apiKey: 'mf_sk_your_secret_key',
1055
+ * baseUrl: 'https://api.example.com/v1',
1056
+ * debug: true
1057
+ * });
1058
+ * ```
1059
+ */
1060
+ constructor(apiKeyOrConfig, options = {}) {
1061
+ this._orgInfo = null;
1062
+ let config;
1063
+ if (typeof apiKeyOrConfig === "string") {
1064
+ config = {
1065
+ apiKey: apiKeyOrConfig,
1066
+ ...options
1067
+ };
1068
+ } else {
1069
+ config = apiKeyOrConfig;
1070
+ }
1071
+ validateApiKey(config.apiKey);
1072
+ this._apiKey = config.apiKey;
1073
+ const keyInfo = parseApiKey(config.apiKey);
1074
+ if (!keyInfo) {
1075
+ throw new ValidationError("Invalid API key format");
1076
+ }
1077
+ this._keyType = keyInfo.type;
1078
+ this.http = new HttpClient(config);
1079
+ this.usage = new UsageResource(this.http);
1080
+ this.customers = new CustomersResource(this.http);
1081
+ this.invoices = new InvoicesResource(this.http);
1082
+ this.analytics = new AnalyticsResource(this.http);
1083
+ this.subscriptions = new SubscriptionsResource(this.http);
1084
+ this.portalSessions = new PortalSessionsResource(
1085
+ this.http,
1086
+ () => this.assertSecretKey()
1087
+ );
1088
+ }
1089
+ /**
1090
+ * Assert that the current API key is a secret key
1091
+ * @throws ValidationError if using a publishable key
1092
+ */
1093
+ assertSecretKey() {
1094
+ if (this._keyType !== "secret") {
1095
+ throw new ValidationError(
1096
+ "This operation requires a secret key (mf_sk_*). Publishable keys (mf_pk_*) cannot perform this action. Use your secret key on the server side only."
1097
+ );
1098
+ }
1099
+ }
1100
+ /**
1101
+ * Connect to the MarginFront API and verify credentials
1102
+ *
1103
+ * This must be called once before using any API methods that require verification.
1104
+ * Optional but recommended for validating your API key.
1105
+ *
1106
+ * @returns Promise resolving to the client instance for chaining
1107
+ * @throws AuthenticationError if API key is invalid
1108
+ * @throws InitializationError for other initialization errors
1109
+ *
1110
+ * @example
1111
+ * ```typescript
1112
+ * const client = new MarginFrontClient('mf_sk_your_secret_key');
1113
+ *
1114
+ * try {
1115
+ * await client.connect();
1116
+ * console.log('Connected to MarginFront API');
1117
+ * } catch (error) {
1118
+ * console.error('Failed to connect:', error);
1119
+ * }
1120
+ * ```
1121
+ */
1122
+ async connect() {
1123
+ try {
1124
+ const result = await this.http.get("/sdk/verify");
1125
+ if (!result || !result.organization || !result.organization.id) {
1126
+ throw new InitializationError("Unexpected response format from API key verification");
1127
+ }
1128
+ this._orgInfo = result.organization;
1129
+ return this;
1130
+ } catch (error) {
1131
+ const err = error;
1132
+ const statusCode = err.statusCode || err.response?.status;
1133
+ const errorMessage = err.message || "Unknown error";
1134
+ const requestId = err.requestId || "unknown";
1135
+ if (statusCode === 401) {
1136
+ throw new AuthenticationError(
1137
+ "Invalid API key. Please check your API key and try again.",
1138
+ { requestId }
1139
+ );
1140
+ }
1141
+ throw new InitializationError(`Connection failed: ${errorMessage}`, { requestId });
1142
+ }
1143
+ }
1144
+ /**
1145
+ * Verify the API key and get organization details
1146
+ *
1147
+ * @returns Verification result with organization info
1148
+ *
1149
+ * @example
1150
+ * ```typescript
1151
+ * const result = await client.verify();
1152
+ * if (result.verified) {
1153
+ * console.log(`Connected to ${result.organization.name}`);
1154
+ * }
1155
+ * ```
1156
+ */
1157
+ async verify() {
1158
+ const result = await this.http.get("/sdk/verify");
1159
+ if (result.organization && !this._orgInfo) {
1160
+ this._orgInfo = result.organization;
1161
+ }
1162
+ return result;
1163
+ }
1164
+ /**
1165
+ * Get organization information
1166
+ *
1167
+ * @returns Organization information from API key verification
1168
+ * @throws Error if organization info is not available (call connect() first)
1169
+ *
1170
+ * @example
1171
+ * ```typescript
1172
+ * await client.connect();
1173
+ * const org = client.getOrganization();
1174
+ * console.log(`Organization: ${org.name}`);
1175
+ * ```
1176
+ */
1177
+ getOrganization() {
1178
+ if (!this._orgInfo) {
1179
+ throw new Error(
1180
+ "Organization information is not available. Make sure the API key is valid and you have called connect()"
1181
+ );
1182
+ }
1183
+ return { ...this._orgInfo };
1184
+ }
1185
+ /**
1186
+ * Track event for a customer (shorthand method)
1187
+ *
1188
+ * @param params - Event tracking parameters
1189
+ * @returns Tracked event data
1190
+ *
1191
+ * @example
1192
+ * ```typescript
1193
+ * // Simple event tracking
1194
+ * await client.trackEvent({
1195
+ * agentId: 'agent_123',
1196
+ * customerExternalId: 'customer_456',
1197
+ * signalName: 'api_call',
1198
+ * quantity: 1
1199
+ * });
1200
+ *
1201
+ * // Batch tracking
1202
+ * await client.trackEvent({
1203
+ * records: [
1204
+ * { customerExternalId: 'cust_1', agentId: 'agent_123', signalName: 'api_call', quantity: 10 },
1205
+ * { customerExternalId: 'cust_2', agentId: 'agent_123', signalName: 'storage', quantity: 100 }
1206
+ * ]
1207
+ * });
1208
+ * ```
1209
+ */
1210
+ async trackEvent(params) {
1211
+ return this.usage.trackEvent(params);
1212
+ }
1213
+ /**
1214
+ * Re-verify the API key
1215
+ *
1216
+ * Useful if you suspect the API key status might have changed
1217
+ *
1218
+ * @returns Updated organization information
1219
+ */
1220
+ async verifyApiKey() {
1221
+ const result = await this.verify();
1222
+ if (!result.organization) {
1223
+ throw new InitializationError("Unexpected response format from API key verification");
1224
+ }
1225
+ this._orgInfo = result.organization;
1226
+ return { ...this._orgInfo };
1227
+ }
1228
+ /**
1229
+ * Get current telemetry statistics
1230
+ *
1231
+ * @returns Telemetry statistics
1232
+ *
1233
+ * @example
1234
+ * ```typescript
1235
+ * const stats = client.getTelemetryStats();
1236
+ * console.log(`Total Requests: ${stats.requestCount}`);
1237
+ * console.log(`Success Rate: ${(stats.successRate * 100).toFixed(2)}%`);
1238
+ * ```
1239
+ */
1240
+ getTelemetryStats() {
1241
+ return this.http.getTelemetryStats();
1242
+ }
1243
+ /**
1244
+ * Reset telemetry statistics
1245
+ */
1246
+ resetTelemetryStats() {
1247
+ this.http.resetTelemetryStats();
1248
+ }
1249
+ /**
1250
+ * Get the API key type (secret or publishable)
1251
+ *
1252
+ * @returns 'secret' for mf_sk_* keys, 'publishable' for mf_pk_* keys
1253
+ *
1254
+ * @example
1255
+ * ```typescript
1256
+ * if (client.getKeyType() === 'secret') {
1257
+ * const session = await client.portalSessions.create({ ... });
1258
+ * }
1259
+ * ```
1260
+ */
1261
+ getKeyType() {
1262
+ return this._keyType;
1263
+ }
1264
+ /**
1265
+ * Check if the API key is a secret key (mf_sk_*)
1266
+ *
1267
+ * @returns true if the key can perform server-side operations
1268
+ */
1269
+ isSecretKey() {
1270
+ return this._keyType === "secret";
1271
+ }
1272
+ /**
1273
+ * Check if the API key is a publishable key (mf_pk_*)
1274
+ *
1275
+ * @returns true if the key is safe for frontend use
1276
+ */
1277
+ isPublishableKey() {
1278
+ return this._keyType === "publishable";
1279
+ }
1280
+ /**
1281
+ * Get a masked version of the API key for debugging
1282
+ *
1283
+ * @returns Masked API key showing only prefix and last 4 characters
1284
+ */
1285
+ getMaskedApiKey() {
1286
+ if (this._apiKey.length <= 20) {
1287
+ return this._apiKey.substring(0, 8) + "...";
1288
+ }
1289
+ return this._apiKey.substring(0, 16) + "..." + this._apiKey.slice(-4);
1290
+ }
1291
+ };
1292
+ export {
1293
+ ApiError,
1294
+ AuthenticationError,
1295
+ AuthorizationError,
1296
+ ConflictError,
1297
+ InitializationError,
1298
+ InternalError,
1299
+ MarginFrontClient,
1300
+ MarginFrontError,
1301
+ NetworkError,
1302
+ NotFoundError,
1303
+ RateLimitError,
1304
+ TimeoutError,
1305
+ ValidationError,
1306
+ createErrorFromResponse,
1307
+ isPublishableKey,
1308
+ isSecretKey,
1309
+ parseApiError,
1310
+ parseApiKey
1311
+ };