proofio-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -0
- package/dist/cjs/client/api-client.js +284 -0
- package/dist/cjs/index.js +74 -0
- package/dist/cjs/resources/competitors.js +45 -0
- package/dist/cjs/resources/insights.js +101 -0
- package/dist/cjs/resources/reviews.js +69 -0
- package/dist/cjs/resources/widget.js +58 -0
- package/dist/cjs/types/index.js +7 -0
- package/dist/cjs/utils/errors.js +50 -0
- package/dist/client/api-client.d.ts +68 -0
- package/dist/client/api-client.d.ts.map +1 -0
- package/dist/esm/client/api-client.js +280 -0
- package/dist/esm/index.js +68 -0
- package/dist/esm/resources/competitors.js +41 -0
- package/dist/esm/resources/insights.js +97 -0
- package/dist/esm/resources/reviews.js +65 -0
- package/dist/esm/resources/widget.js +54 -0
- package/dist/esm/types/index.js +6 -0
- package/dist/esm/utils/errors.js +46 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/resources/competitors.d.ts +33 -0
- package/dist/resources/competitors.d.ts.map +1 -0
- package/dist/resources/insights.d.ts +46 -0
- package/dist/resources/insights.d.ts.map +1 -0
- package/dist/resources/reviews.d.ts +29 -0
- package/dist/resources/reviews.d.ts.map +1 -0
- package/dist/resources/widget.d.ts +44 -0
- package/dist/resources/widget.d.ts.map +1 -0
- package/dist/types/index.d.ts +217 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Competitors Resource
|
|
4
|
+
*
|
|
5
|
+
* Handles competitor comparison functionality
|
|
6
|
+
*
|
|
7
|
+
* Note: Competitor comparison requires authentication and is typically
|
|
8
|
+
* accessed via the dashboard API. This resource provides a placeholder
|
|
9
|
+
* structure that matches the SDK API style.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CompetitorsResource = void 0;
|
|
13
|
+
class CompetitorsResource {
|
|
14
|
+
constructor(client) {
|
|
15
|
+
this.client = client;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compare with a competitor
|
|
19
|
+
*
|
|
20
|
+
* Generates an AI-powered comparison between your product and a competitor.
|
|
21
|
+
*
|
|
22
|
+
* Note: This endpoint requires dashboard API access (authentication).
|
|
23
|
+
* The public API doesn't support competitor comparisons directly.
|
|
24
|
+
* This method is provided for API consistency but may require
|
|
25
|
+
* additional authentication setup.
|
|
26
|
+
*
|
|
27
|
+
* @param competitorId - Competitor ID
|
|
28
|
+
* @param options - Optional parameters (e.g., force refresh)
|
|
29
|
+
* @returns Promise with competitor comparison
|
|
30
|
+
*/
|
|
31
|
+
async compare(competitorId, options) {
|
|
32
|
+
const queryParams = {};
|
|
33
|
+
if (options?.force) {
|
|
34
|
+
queryParams.force = 'true';
|
|
35
|
+
}
|
|
36
|
+
// Note: This endpoint typically requires authentication
|
|
37
|
+
// The public API doesn't expose competitor comparisons
|
|
38
|
+
// This is a placeholder that matches the expected API structure
|
|
39
|
+
const response = await this.client.get(`/api/dashboard/competitors/${competitorId}/comparison`, {
|
|
40
|
+
queryParams,
|
|
41
|
+
});
|
|
42
|
+
return response.data;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.CompetitorsResource = CompetitorsResource;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Insights Resource
|
|
4
|
+
*
|
|
5
|
+
* Handles insights, summaries, and trend analysis
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.InsightsResource = void 0;
|
|
9
|
+
class InsightsResource {
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get insight summary
|
|
15
|
+
*
|
|
16
|
+
* Returns aggregated statistics including:
|
|
17
|
+
* - Total reviews
|
|
18
|
+
* - Average rating
|
|
19
|
+
* - Rating distribution
|
|
20
|
+
* - Sentiment distribution
|
|
21
|
+
* - Source breakdown
|
|
22
|
+
* - AI summary (if available for paid plans)
|
|
23
|
+
*
|
|
24
|
+
* @returns Promise with insight summary
|
|
25
|
+
*/
|
|
26
|
+
async summary() {
|
|
27
|
+
const response = await this.client.get('/api/v1/public/aggregations');
|
|
28
|
+
return response.data;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get trend analysis
|
|
32
|
+
*
|
|
33
|
+
* Returns detailed trend data including:
|
|
34
|
+
* - Trend over time (last 30 days)
|
|
35
|
+
* - Review volume (7, 30, 90 days)
|
|
36
|
+
* - Top topics
|
|
37
|
+
* - Key takeaways
|
|
38
|
+
* - Recent changes
|
|
39
|
+
*
|
|
40
|
+
* Note: The public API provides limited trend data. For full trend analysis
|
|
41
|
+
* including trendOverTime, topTopics, and recentChanges, you need access
|
|
42
|
+
* to the dashboard API (requires authentication).
|
|
43
|
+
*
|
|
44
|
+
* This method returns available data from the public aggregations endpoint
|
|
45
|
+
* and sets default/empty values for fields not available in the public API.
|
|
46
|
+
*
|
|
47
|
+
* @returns Promise with trend data
|
|
48
|
+
*/
|
|
49
|
+
async trends() {
|
|
50
|
+
// Get data from public aggregations endpoint
|
|
51
|
+
const response = await this.client.get('/api/v1/public/aggregations');
|
|
52
|
+
const data = response.data;
|
|
53
|
+
// Calculate positive percentage
|
|
54
|
+
const positivePercentage = data.totalReviews > 0
|
|
55
|
+
? (data.sentimentDistribution.positive / data.totalReviews) * 100
|
|
56
|
+
: 0;
|
|
57
|
+
// Transform to InsightTrends format
|
|
58
|
+
// Note: The public API doesn't provide all trend fields,
|
|
59
|
+
// so we provide what's available and set defaults for missing fields
|
|
60
|
+
return {
|
|
61
|
+
totalReviews: data.totalReviews,
|
|
62
|
+
averageRating: data.averageRating,
|
|
63
|
+
ratingDistribution: data.ratingDistribution,
|
|
64
|
+
sentimentDistribution: data.sentimentDistribution,
|
|
65
|
+
thisWeekCount: 0, // Not available in public API
|
|
66
|
+
thisWeekChange: 0, // Not available in public API
|
|
67
|
+
positivePercentage: Math.round(positivePercentage * 10) / 10,
|
|
68
|
+
reviewVolume: {
|
|
69
|
+
last7Days: 0, // Not available in public API
|
|
70
|
+
last30Days: 0, // Not available in public API
|
|
71
|
+
last90Days: 0, // Not available in public API
|
|
72
|
+
total: data.totalReviews,
|
|
73
|
+
},
|
|
74
|
+
sourceBreakdown: data.sources.reduce((acc, source) => {
|
|
75
|
+
acc[source.id] = {
|
|
76
|
+
count: source.total,
|
|
77
|
+
avgRating: source.averageRating,
|
|
78
|
+
sentiment: {
|
|
79
|
+
positive: 0, // Not available in public API
|
|
80
|
+
neutral: 0,
|
|
81
|
+
negative: 0,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return acc;
|
|
85
|
+
}, {}),
|
|
86
|
+
sourcesMap: data.sources.reduce((acc, source) => {
|
|
87
|
+
acc[source.id] = {
|
|
88
|
+
name: source.name || source.type || 'Unknown',
|
|
89
|
+
type: source.type || 'UNKNOWN',
|
|
90
|
+
};
|
|
91
|
+
return acc;
|
|
92
|
+
}, {}),
|
|
93
|
+
trendOverTime: [], // Not available in public API - requires dashboard API
|
|
94
|
+
topTopics: [], // Not available in public API - requires dashboard API
|
|
95
|
+
keyTakeaways: [], // Not available in public API - requires dashboard API
|
|
96
|
+
recentChanges: [], // Not available in public API - requires dashboard API
|
|
97
|
+
lastSync: null, // Not available in public API
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.InsightsResource = InsightsResource;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reviews Resource
|
|
4
|
+
*
|
|
5
|
+
* Handles all review-related API calls
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ReviewsResource = void 0;
|
|
9
|
+
class ReviewsResource {
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* List reviews
|
|
15
|
+
*
|
|
16
|
+
* @param options - Filter and pagination options
|
|
17
|
+
* @returns Promise with array of reviews
|
|
18
|
+
*/
|
|
19
|
+
async list(options) {
|
|
20
|
+
const queryParams = {};
|
|
21
|
+
if (options?.limit !== undefined) {
|
|
22
|
+
queryParams.limit = Math.min(options.limit, 100); // Max 100 per API
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
queryParams.limit = 10; // Default
|
|
26
|
+
}
|
|
27
|
+
if (options?.offset !== undefined) {
|
|
28
|
+
queryParams.offset = options.offset;
|
|
29
|
+
}
|
|
30
|
+
if (options?.minRating !== undefined) {
|
|
31
|
+
queryParams.minRating = options.minRating;
|
|
32
|
+
}
|
|
33
|
+
if (options?.maxRating !== undefined) {
|
|
34
|
+
queryParams.maxRating = options.maxRating;
|
|
35
|
+
}
|
|
36
|
+
if (options?.sentiment) {
|
|
37
|
+
queryParams.sentiment = options.sentiment;
|
|
38
|
+
}
|
|
39
|
+
if (options?.language) {
|
|
40
|
+
queryParams.language = options.language;
|
|
41
|
+
}
|
|
42
|
+
if (options?.sourceId) {
|
|
43
|
+
queryParams.sourceId = options.sourceId;
|
|
44
|
+
}
|
|
45
|
+
if (options?.since) {
|
|
46
|
+
queryParams.since = options.since;
|
|
47
|
+
}
|
|
48
|
+
const response = await this.client.get('/api/v1/public/reviews', {
|
|
49
|
+
queryParams,
|
|
50
|
+
});
|
|
51
|
+
return response.data;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a single review by ID
|
|
55
|
+
*
|
|
56
|
+
* Note: The API doesn't have a direct GET /reviews/:id endpoint,
|
|
57
|
+
* so we fetch the list and filter. This is a convenience method.
|
|
58
|
+
*
|
|
59
|
+
* @param id - Review ID
|
|
60
|
+
* @returns Promise with review or null if not found
|
|
61
|
+
*/
|
|
62
|
+
async get(id) {
|
|
63
|
+
// Since there's no direct GET endpoint, we need to fetch and filter
|
|
64
|
+
// In a real implementation, you might want to cache reviews or use a different approach
|
|
65
|
+
const reviews = await this.list({ limit: 100 });
|
|
66
|
+
return reviews.find((review) => review.id === id) || null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.ReviewsResource = ReviewsResource;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Widget Resource
|
|
4
|
+
*
|
|
5
|
+
* Handles widget-related API calls
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.WidgetResource = void 0;
|
|
9
|
+
class WidgetResource {
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get widget data
|
|
15
|
+
*
|
|
16
|
+
* Returns widget statistics and settings including:
|
|
17
|
+
* - Total reviews
|
|
18
|
+
* - Average rating
|
|
19
|
+
* - Number of platforms
|
|
20
|
+
* - Widget configuration (language, theme, badges, branding)
|
|
21
|
+
*
|
|
22
|
+
* @returns Promise with widget data
|
|
23
|
+
*/
|
|
24
|
+
async get() {
|
|
25
|
+
const response = await this.client.get('/api/v1/public/widget');
|
|
26
|
+
return response.data;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get widget configuration
|
|
30
|
+
*
|
|
31
|
+
* Returns only the widget settings/configuration.
|
|
32
|
+
* This is a convenience method that extracts settings from get().
|
|
33
|
+
*
|
|
34
|
+
* @returns Promise with widget settings
|
|
35
|
+
*/
|
|
36
|
+
async config() {
|
|
37
|
+
const data = await this.get();
|
|
38
|
+
return data.settings;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Update widget configuration
|
|
42
|
+
*
|
|
43
|
+
* Note: Widget configuration updates typically require dashboard API access.
|
|
44
|
+
* This method is provided for API consistency but may require
|
|
45
|
+
* additional authentication setup.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Widget configuration options
|
|
48
|
+
* @returns Promise with updated widget settings
|
|
49
|
+
*/
|
|
50
|
+
async updateConfig(options) {
|
|
51
|
+
// Note: This endpoint typically requires authentication
|
|
52
|
+
// The public API doesn't support widget config updates
|
|
53
|
+
// This is a placeholder that matches the expected API structure
|
|
54
|
+
const response = await this.client.post('/api/dashboard/widget-settings', options);
|
|
55
|
+
return response.data;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.WidgetResource = WidgetResource;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Proofio Error Handling
|
|
4
|
+
*
|
|
5
|
+
* Normalized error class for all SDK errors
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ProofioError = void 0;
|
|
9
|
+
class ProofioError extends Error {
|
|
10
|
+
constructor(data) {
|
|
11
|
+
super(data.message);
|
|
12
|
+
this.name = 'ProofioError';
|
|
13
|
+
this.status = data.status;
|
|
14
|
+
this.code = data.code;
|
|
15
|
+
this.requestId = data.requestId;
|
|
16
|
+
this.retryAfter = data.retryAfter;
|
|
17
|
+
this.rateLimitRemaining = data.rateLimitRemaining;
|
|
18
|
+
this.rateLimitReset = data.rateLimitReset;
|
|
19
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8/Node.js)
|
|
20
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
21
|
+
Error.captureStackTrace(this, ProofioError);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if error is retryable
|
|
26
|
+
*/
|
|
27
|
+
isRetryable() {
|
|
28
|
+
return (this.status >= 500 ||
|
|
29
|
+
this.status === 429 ||
|
|
30
|
+
this.status === 408 ||
|
|
31
|
+
this.status === 0 // Network error
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert to JSON (sanitized, no sensitive data)
|
|
36
|
+
*/
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
name: this.name,
|
|
40
|
+
message: this.message,
|
|
41
|
+
status: this.status,
|
|
42
|
+
code: this.code,
|
|
43
|
+
requestId: this.requestId,
|
|
44
|
+
retryAfter: this.retryAfter,
|
|
45
|
+
rateLimitRemaining: this.rateLimitRemaining,
|
|
46
|
+
rateLimitReset: this.rateLimitReset,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.ProofioError = ProofioError;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core API Client für Proofio SDK
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - API Key Authentication
|
|
6
|
+
* - Base URL Management
|
|
7
|
+
* - Request/Response Handling
|
|
8
|
+
* - Error Normalization
|
|
9
|
+
* - Rate Limit Handling
|
|
10
|
+
* - Retry Logic
|
|
11
|
+
*/
|
|
12
|
+
import type { RequestOptions, Response } from '../types';
|
|
13
|
+
export interface ProofioConfig {
|
|
14
|
+
apiKey: string;
|
|
15
|
+
baseURL?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
retryDelay?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class ApiClient {
|
|
21
|
+
private apiKey;
|
|
22
|
+
private baseURL;
|
|
23
|
+
private timeout;
|
|
24
|
+
private maxRetries;
|
|
25
|
+
private retryDelay;
|
|
26
|
+
constructor(config: ProofioConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Sanitize API key for logging (never log full key)
|
|
29
|
+
*/
|
|
30
|
+
private sanitizeApiKey;
|
|
31
|
+
/**
|
|
32
|
+
* Build headers for requests
|
|
33
|
+
*/
|
|
34
|
+
private buildHeaders;
|
|
35
|
+
/**
|
|
36
|
+
* Build full URL
|
|
37
|
+
*/
|
|
38
|
+
private buildURL;
|
|
39
|
+
/**
|
|
40
|
+
* Handle rate limit errors
|
|
41
|
+
*/
|
|
42
|
+
private handleRateLimit;
|
|
43
|
+
/**
|
|
44
|
+
* Parse error response
|
|
45
|
+
*/
|
|
46
|
+
private parseErrorResponse;
|
|
47
|
+
/**
|
|
48
|
+
* Sleep helper for retries
|
|
49
|
+
*/
|
|
50
|
+
private sleep;
|
|
51
|
+
/**
|
|
52
|
+
* Check if error is retryable
|
|
53
|
+
*/
|
|
54
|
+
private isRetryableError;
|
|
55
|
+
/**
|
|
56
|
+
* Execute request with retry logic
|
|
57
|
+
*/
|
|
58
|
+
private executeWithRetry;
|
|
59
|
+
/**
|
|
60
|
+
* Make GET request
|
|
61
|
+
*/
|
|
62
|
+
get<T>(path: string, options?: RequestOptions): Promise<Response<T>>;
|
|
63
|
+
/**
|
|
64
|
+
* Make POST request
|
|
65
|
+
*/
|
|
66
|
+
post<T>(path: string, body?: any, options?: RequestOptions): Promise<Response<T>>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/client/api-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,UAAU,CAAQ;gBAEd,MAAM,EAAE,aAAa;IAyBjC;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,OAAO,CAAC,YAAY;IAYpB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAchB;;OAEG;YACW,eAAe;IAwB7B;;OAEG;YACW,kBAAkB;IAyBhC;;OAEG;IACH,OAAO,CAAC,KAAK;IAIb;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;YACW,gBAAgB;IAgC9B;;OAEG;IACG,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IA6D1E;;OAEG;IACG,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;CA6DxF"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core API Client für Proofio SDK
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - API Key Authentication
|
|
6
|
+
* - Base URL Management
|
|
7
|
+
* - Request/Response Handling
|
|
8
|
+
* - Error Normalization
|
|
9
|
+
* - Rate Limit Handling
|
|
10
|
+
* - Retry Logic
|
|
11
|
+
*/
|
|
12
|
+
import { ProofioError } from '../utils/errors';
|
|
13
|
+
export class ApiClient {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
if (!config.apiKey || typeof config.apiKey !== 'string' || config.apiKey.trim().length === 0) {
|
|
16
|
+
throw new ProofioError({
|
|
17
|
+
message: 'API key is required',
|
|
18
|
+
status: 0,
|
|
19
|
+
code: 'INVALID_API_KEY',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Validate API key format (basic validation)
|
|
23
|
+
if (config.apiKey.length < 10) {
|
|
24
|
+
throw new ProofioError({
|
|
25
|
+
message: 'API key format is invalid',
|
|
26
|
+
status: 0,
|
|
27
|
+
code: 'INVALID_API_KEY',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
this.apiKey = config.apiKey.trim();
|
|
31
|
+
this.baseURL = config.baseURL || 'https://proofio.app';
|
|
32
|
+
this.timeout = config.timeout || 30000; // 30 seconds
|
|
33
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
34
|
+
this.retryDelay = config.retryDelay ?? 1000; // 1 second
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sanitize API key for logging (never log full key)
|
|
38
|
+
*/
|
|
39
|
+
sanitizeApiKey() {
|
|
40
|
+
if (this.apiKey.length <= 8) {
|
|
41
|
+
return '***';
|
|
42
|
+
}
|
|
43
|
+
return `${this.apiKey.substring(0, 4)}...${this.apiKey.substring(this.apiKey.length - 4)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build headers for requests
|
|
47
|
+
*/
|
|
48
|
+
buildHeaders(customHeaders) {
|
|
49
|
+
const headers = new Headers({
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'x-api-key': this.apiKey,
|
|
52
|
+
'User-Agent': 'proofio-sdk/1.0.0',
|
|
53
|
+
...customHeaders,
|
|
54
|
+
});
|
|
55
|
+
// Make headers immutable by creating new Headers object
|
|
56
|
+
return new Headers(headers);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build full URL
|
|
60
|
+
*/
|
|
61
|
+
buildURL(path, queryParams) {
|
|
62
|
+
const url = new URL(path, this.baseURL);
|
|
63
|
+
if (queryParams) {
|
|
64
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
65
|
+
if (value !== undefined && value !== null) {
|
|
66
|
+
url.searchParams.append(key, String(value));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return url.toString();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Handle rate limit errors
|
|
74
|
+
*/
|
|
75
|
+
async handleRateLimit(response) {
|
|
76
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
77
|
+
const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
|
|
78
|
+
const rateLimitReset = response.headers.get('X-RateLimit-Reset');
|
|
79
|
+
const errorData = {
|
|
80
|
+
message: 'Rate limit exceeded',
|
|
81
|
+
status: 429,
|
|
82
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
83
|
+
};
|
|
84
|
+
if (retryAfter) {
|
|
85
|
+
errorData.retryAfter = parseInt(retryAfter, 10);
|
|
86
|
+
}
|
|
87
|
+
if (rateLimitRemaining !== null) {
|
|
88
|
+
errorData.rateLimitRemaining = parseInt(rateLimitRemaining, 10);
|
|
89
|
+
}
|
|
90
|
+
if (rateLimitReset) {
|
|
91
|
+
errorData.rateLimitReset = parseInt(rateLimitReset, 10);
|
|
92
|
+
}
|
|
93
|
+
throw new ProofioError(errorData);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse error response
|
|
97
|
+
*/
|
|
98
|
+
async parseErrorResponse(response) {
|
|
99
|
+
let errorData = {
|
|
100
|
+
message: `API request failed with status ${response.status}`,
|
|
101
|
+
status: response.status,
|
|
102
|
+
code: 'API_ERROR',
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
const contentType = response.headers.get('content-type');
|
|
106
|
+
if (contentType && contentType.includes('application/json')) {
|
|
107
|
+
const data = await response.json();
|
|
108
|
+
errorData = {
|
|
109
|
+
...errorData,
|
|
110
|
+
message: data.error || data.message || errorData.message,
|
|
111
|
+
code: data.code || errorData.code,
|
|
112
|
+
requestId: data.requestId,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// If JSON parsing fails, use default error
|
|
118
|
+
}
|
|
119
|
+
return new ProofioError(errorData);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sleep helper for retries
|
|
123
|
+
*/
|
|
124
|
+
sleep(ms) {
|
|
125
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if error is retryable
|
|
129
|
+
*/
|
|
130
|
+
isRetryableError(status, error) {
|
|
131
|
+
// Retry on network errors, timeouts, and 5xx errors
|
|
132
|
+
if (!status)
|
|
133
|
+
return true; // Network error
|
|
134
|
+
if (status >= 500)
|
|
135
|
+
return true; // Server errors
|
|
136
|
+
if (status === 429)
|
|
137
|
+
return true; // Rate limit (with backoff)
|
|
138
|
+
if (status === 408)
|
|
139
|
+
return true; // Request timeout
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Execute request with retry logic
|
|
144
|
+
*/
|
|
145
|
+
async executeWithRetry(requestFn, retryCount = 0) {
|
|
146
|
+
try {
|
|
147
|
+
const response = await requestFn();
|
|
148
|
+
// If rate limited, throw immediately (don't retry immediately)
|
|
149
|
+
if (response.status === 429) {
|
|
150
|
+
await this.handleRateLimit(response);
|
|
151
|
+
}
|
|
152
|
+
// If error is retryable and we haven't exceeded max retries
|
|
153
|
+
if (!response.ok && this.isRetryableError(response.status) && retryCount < this.maxRetries) {
|
|
154
|
+
// Exponential backoff with jitter
|
|
155
|
+
const delay = this.retryDelay * Math.pow(2, retryCount) + Math.random() * 1000;
|
|
156
|
+
await this.sleep(delay);
|
|
157
|
+
return this.executeWithRetry(requestFn, retryCount + 1);
|
|
158
|
+
}
|
|
159
|
+
return response;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
// Network errors are retryable
|
|
163
|
+
if (retryCount < this.maxRetries && this.isRetryableError(0, error)) {
|
|
164
|
+
const delay = this.retryDelay * Math.pow(2, retryCount) + Math.random() * 1000;
|
|
165
|
+
await this.sleep(delay);
|
|
166
|
+
return this.executeWithRetry(requestFn, retryCount + 1);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Make GET request
|
|
173
|
+
*/
|
|
174
|
+
async get(path, options) {
|
|
175
|
+
const url = this.buildURL(path, options?.queryParams);
|
|
176
|
+
const headers = this.buildHeaders(options?.headers);
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
179
|
+
try {
|
|
180
|
+
const response = await this.executeWithRetry(async () => {
|
|
181
|
+
return fetch(url, {
|
|
182
|
+
method: 'GET',
|
|
183
|
+
headers,
|
|
184
|
+
signal: controller.signal,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
clearTimeout(timeoutId);
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
if (response.status === 429) {
|
|
190
|
+
await this.handleRateLimit(response);
|
|
191
|
+
}
|
|
192
|
+
throw await this.parseErrorResponse(response);
|
|
193
|
+
}
|
|
194
|
+
const data = await response.json();
|
|
195
|
+
// Convert Headers to plain object
|
|
196
|
+
const headers = {};
|
|
197
|
+
response.headers.forEach((value, key) => {
|
|
198
|
+
headers[key] = value;
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
data,
|
|
202
|
+
status: response.status,
|
|
203
|
+
headers,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
clearTimeout(timeoutId);
|
|
208
|
+
if (error instanceof ProofioError) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
212
|
+
throw new ProofioError({
|
|
213
|
+
message: 'Request timeout',
|
|
214
|
+
status: 408,
|
|
215
|
+
code: 'TIMEOUT',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
throw new ProofioError({
|
|
219
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
220
|
+
status: 0,
|
|
221
|
+
code: 'NETWORK_ERROR',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Make POST request
|
|
227
|
+
*/
|
|
228
|
+
async post(path, body, options) {
|
|
229
|
+
const url = this.buildURL(path, options?.queryParams);
|
|
230
|
+
const headers = this.buildHeaders(options?.headers);
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
233
|
+
try {
|
|
234
|
+
const response = await this.executeWithRetry(async () => {
|
|
235
|
+
return fetch(url, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers,
|
|
238
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
239
|
+
signal: controller.signal,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
clearTimeout(timeoutId);
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
if (response.status === 429) {
|
|
245
|
+
await this.handleRateLimit(response);
|
|
246
|
+
}
|
|
247
|
+
throw await this.parseErrorResponse(response);
|
|
248
|
+
}
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
// Convert Headers to plain object
|
|
251
|
+
const headers = {};
|
|
252
|
+
response.headers.forEach((value, key) => {
|
|
253
|
+
headers[key] = value;
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
data,
|
|
257
|
+
status: response.status,
|
|
258
|
+
headers,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
clearTimeout(timeoutId);
|
|
263
|
+
if (error instanceof ProofioError) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
267
|
+
throw new ProofioError({
|
|
268
|
+
message: 'Request timeout',
|
|
269
|
+
status: 408,
|
|
270
|
+
code: 'TIMEOUT',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
throw new ProofioError({
|
|
274
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
275
|
+
status: 0,
|
|
276
|
+
code: 'NETWORK_ERROR',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|