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