@fedpulse/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.
Potentially problematic release.
This version of @fedpulse/sdk might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +331 -0
- package/dist/cjs/client.cjs +138 -0
- package/dist/cjs/errors.cjs +200 -0
- package/dist/cjs/http.cjs +449 -0
- package/dist/cjs/index.cjs +65 -0
- package/dist/cjs/resources/analytics.cjs +134 -0
- package/dist/cjs/resources/assistance.cjs +101 -0
- package/dist/cjs/resources/entities.cjs +149 -0
- package/dist/cjs/resources/exclusions.cjs +135 -0
- package/dist/cjs/resources/intelligence.cjs +96 -0
- package/dist/cjs/resources/opportunities.cjs +170 -0
- package/dist/cjs/resources/webhooks.cjs +262 -0
- package/dist/cjs/types/analytics.cjs +5 -0
- package/dist/cjs/types/assistance.cjs +5 -0
- package/dist/cjs/types/common.cjs +5 -0
- package/dist/cjs/types/entities.cjs +5 -0
- package/dist/cjs/types/exclusions.cjs +5 -0
- package/dist/cjs/types/index.cjs +5 -0
- package/dist/cjs/types/intelligence.cjs +5 -0
- package/dist/cjs/types/opportunities.cjs +5 -0
- package/dist/cjs/types/webhooks.cjs +5 -0
- package/dist/cjs/webhooks-verify.cjs +184 -0
- package/dist/esm/client.js +135 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/errors.js +187 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/http.js +445 -0
- package/dist/esm/http.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/resources/analytics.js +131 -0
- package/dist/esm/resources/analytics.js.map +1 -0
- package/dist/esm/resources/assistance.js +98 -0
- package/dist/esm/resources/assistance.js.map +1 -0
- package/dist/esm/resources/entities.js +146 -0
- package/dist/esm/resources/entities.js.map +1 -0
- package/dist/esm/resources/exclusions.js +132 -0
- package/dist/esm/resources/exclusions.js.map +1 -0
- package/dist/esm/resources/intelligence.js +93 -0
- package/dist/esm/resources/intelligence.js.map +1 -0
- package/dist/esm/resources/opportunities.js +167 -0
- package/dist/esm/resources/opportunities.js.map +1 -0
- package/dist/esm/resources/webhooks.js +259 -0
- package/dist/esm/resources/webhooks.js.map +1 -0
- package/dist/esm/types/analytics.js +5 -0
- package/dist/esm/types/analytics.js.map +1 -0
- package/dist/esm/types/assistance.js +5 -0
- package/dist/esm/types/assistance.js.map +1 -0
- package/dist/esm/types/common.js +5 -0
- package/dist/esm/types/common.js.map +1 -0
- package/dist/esm/types/entities.js +5 -0
- package/dist/esm/types/entities.js.map +1 -0
- package/dist/esm/types/exclusions.js +5 -0
- package/dist/esm/types/exclusions.js.map +1 -0
- package/dist/esm/types/index.js +5 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/types/intelligence.js +5 -0
- package/dist/esm/types/intelligence.js.map +1 -0
- package/dist/esm/types/opportunities.js +5 -0
- package/dist/esm/types/opportunities.js.map +1 -0
- package/dist/esm/types/webhooks.js +5 -0
- package/dist/esm/types/webhooks.js.map +1 -0
- package/dist/esm/webhooks-verify.js +179 -0
- package/dist/esm/webhooks-verify.js.map +1 -0
- package/dist/types/client.d.cts +136 -0
- package/dist/types/client.d.ts +136 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/errors.d.cts +139 -0
- package/dist/types/errors.d.ts +139 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/http.d.cts +137 -0
- package/dist/types/http.d.ts +137 -0
- package/dist/types/http.d.ts.map +1 -0
- package/dist/types/index.d.cts +39 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/resources/analytics.d.cts +94 -0
- package/dist/types/resources/analytics.d.ts +94 -0
- package/dist/types/resources/analytics.d.ts.map +1 -0
- package/dist/types/resources/assistance.d.cts +66 -0
- package/dist/types/resources/assistance.d.ts +66 -0
- package/dist/types/resources/assistance.d.ts.map +1 -0
- package/dist/types/resources/entities.d.cts +101 -0
- package/dist/types/resources/entities.d.ts +101 -0
- package/dist/types/resources/entities.d.ts.map +1 -0
- package/dist/types/resources/exclusions.d.cts +84 -0
- package/dist/types/resources/exclusions.d.ts +84 -0
- package/dist/types/resources/exclusions.d.ts.map +1 -0
- package/dist/types/resources/intelligence.d.cts +66 -0
- package/dist/types/resources/intelligence.d.ts +66 -0
- package/dist/types/resources/intelligence.d.ts.map +1 -0
- package/dist/types/resources/opportunities.d.cts +116 -0
- package/dist/types/resources/opportunities.d.ts +116 -0
- package/dist/types/resources/opportunities.d.ts.map +1 -0
- package/dist/types/resources/webhooks.d.cts +180 -0
- package/dist/types/resources/webhooks.d.ts +180 -0
- package/dist/types/resources/webhooks.d.ts.map +1 -0
- package/dist/types/types/analytics.d.cts +85 -0
- package/dist/types/types/analytics.d.ts +85 -0
- package/dist/types/types/analytics.d.ts.map +1 -0
- package/dist/types/types/assistance.d.cts +55 -0
- package/dist/types/types/assistance.d.ts +55 -0
- package/dist/types/types/assistance.d.ts.map +1 -0
- package/dist/types/types/common.d.cts +58 -0
- package/dist/types/types/common.d.ts +58 -0
- package/dist/types/types/common.d.ts.map +1 -0
- package/dist/types/types/entities.d.cts +85 -0
- package/dist/types/types/entities.d.ts +85 -0
- package/dist/types/types/entities.d.ts.map +1 -0
- package/dist/types/types/exclusions.d.cts +81 -0
- package/dist/types/types/exclusions.d.ts +81 -0
- package/dist/types/types/exclusions.d.ts.map +1 -0
- package/dist/types/types/index.d.cts +12 -0
- package/dist/types/types/index.d.ts +12 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/types/intelligence.d.cts +104 -0
- package/dist/types/types/intelligence.d.ts +104 -0
- package/dist/types/types/intelligence.d.ts.map +1 -0
- package/dist/types/types/opportunities.d.cts +149 -0
- package/dist/types/types/opportunities.d.ts +149 -0
- package/dist/types/types/opportunities.d.ts.map +1 -0
- package/dist/types/types/webhooks.d.cts +106 -0
- package/dist/types/types/webhooks.d.ts +106 -0
- package/dist/types/types/webhooks.d.ts.map +1 -0
- package/dist/types/webhooks-verify.d.cts +102 -0
- package/dist/types/webhooks-verify.d.ts +102 -0
- package/dist/types/webhooks-verify.d.ts.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core HTTP client for the FedPulse SDK.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Native fetch (Node 18+) — zero runtime dependencies
|
|
7
|
+
* - Configurable timeout via AbortController
|
|
8
|
+
* - Automatic retry with exponential back-off + full jitter for transient errors
|
|
9
|
+
* - In-memory LRU cache for GET requests with per-entry TTL
|
|
10
|
+
* - Rate-limit header tracking (X-RateLimit-*)
|
|
11
|
+
* - Standard API envelope parsing + typed error creation
|
|
12
|
+
* - Array query-parameter serialisation (repeated keys)
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.HttpClient = void 0;
|
|
16
|
+
exports.buildQueryParams = buildQueryParams;
|
|
17
|
+
const errors_js_1 = require("./errors.cjs");
|
|
18
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
19
|
+
const DEFAULT_BASE_URL = 'https://api.fedpulse.dev';
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
22
|
+
const DEFAULT_CACHE_SIZE = 256;
|
|
23
|
+
const DEFAULT_CACHE_TTL_MS = 60_000;
|
|
24
|
+
/** HTTP status codes that are safe to retry (transient server-side errors). */
|
|
25
|
+
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
26
|
+
/**
|
|
27
|
+
* Least-recently-used cache with per-entry TTL.
|
|
28
|
+
* Implemented with a doubly-linked list + Map for O(1) get and set.
|
|
29
|
+
*
|
|
30
|
+
* @template T Cached value type.
|
|
31
|
+
*/
|
|
32
|
+
class LruCache {
|
|
33
|
+
capacity;
|
|
34
|
+
defaultTtlMs;
|
|
35
|
+
map = new Map();
|
|
36
|
+
// Sentinel nodes avoid null-checks on boundary operations.
|
|
37
|
+
head;
|
|
38
|
+
tail;
|
|
39
|
+
constructor(capacity, defaultTtlMs) {
|
|
40
|
+
this.capacity = capacity;
|
|
41
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
42
|
+
this.head = { value: null, expiresAt: 0, key: '__head', prev: null, next: null };
|
|
43
|
+
this.tail = { value: null, expiresAt: 0, key: '__tail', prev: null, next: null };
|
|
44
|
+
this.head.next = this.tail;
|
|
45
|
+
this.tail.prev = this.head;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Retrieve a cached value. Returns undefined on miss or expiry.
|
|
49
|
+
*/
|
|
50
|
+
get(key) {
|
|
51
|
+
const entry = this.map.get(key);
|
|
52
|
+
if (entry === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (Date.now() > entry.expiresAt) {
|
|
55
|
+
this.remove(entry);
|
|
56
|
+
this.map.delete(key);
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
// Move to front (most-recently-used).
|
|
60
|
+
this.remove(entry);
|
|
61
|
+
this.insertFront(entry);
|
|
62
|
+
return entry.value;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Store a value. Returns the evicted key if capacity was exceeded.
|
|
66
|
+
*/
|
|
67
|
+
set(key, value, ttlMs) {
|
|
68
|
+
// Evict stale entry for the same key first.
|
|
69
|
+
const existing = this.map.get(key);
|
|
70
|
+
if (existing !== undefined) {
|
|
71
|
+
this.remove(existing);
|
|
72
|
+
}
|
|
73
|
+
const entry = {
|
|
74
|
+
key,
|
|
75
|
+
value,
|
|
76
|
+
expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
|
|
77
|
+
prev: null,
|
|
78
|
+
next: null,
|
|
79
|
+
};
|
|
80
|
+
this.map.set(key, entry);
|
|
81
|
+
this.insertFront(entry);
|
|
82
|
+
// Evict LRU entry if over capacity.
|
|
83
|
+
if (this.map.size > this.capacity) {
|
|
84
|
+
// LRU entry is just before the tail sentinel.
|
|
85
|
+
const lru = this.tail.prev;
|
|
86
|
+
if (lru !== this.head) {
|
|
87
|
+
this.remove(lru);
|
|
88
|
+
this.map.delete(lru.key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Delete a specific key from the cache. */
|
|
93
|
+
delete(key) {
|
|
94
|
+
const entry = this.map.get(key);
|
|
95
|
+
if (entry !== undefined) {
|
|
96
|
+
this.remove(entry);
|
|
97
|
+
this.map.delete(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Remove all entries. */
|
|
101
|
+
clear() {
|
|
102
|
+
this.map.clear();
|
|
103
|
+
this.head.next = this.tail;
|
|
104
|
+
this.tail.prev = this.head;
|
|
105
|
+
}
|
|
106
|
+
/** Number of entries currently in the cache. */
|
|
107
|
+
get size() {
|
|
108
|
+
return this.map.size;
|
|
109
|
+
}
|
|
110
|
+
insertFront(entry) {
|
|
111
|
+
entry.prev = this.head;
|
|
112
|
+
entry.next = this.head.next;
|
|
113
|
+
this.head.next.prev = entry;
|
|
114
|
+
this.head.next = entry;
|
|
115
|
+
}
|
|
116
|
+
remove(entry) {
|
|
117
|
+
entry.prev.next = entry.next;
|
|
118
|
+
entry.next.prev = entry.prev;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── Query string serialisation ─────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Serialise a plain-object params map to a URLSearchParams instance.
|
|
124
|
+
*
|
|
125
|
+
* Arrays are serialised as repeated keys: `{ naics: ['11', '21'] }` →
|
|
126
|
+
* `?naics=11&naics=21`.
|
|
127
|
+
*
|
|
128
|
+
* Undefined and null values are omitted.
|
|
129
|
+
*/
|
|
130
|
+
function buildQueryParams(params) {
|
|
131
|
+
const qs = new URLSearchParams();
|
|
132
|
+
for (const [key, value] of Object.entries(params)) {
|
|
133
|
+
if (value === undefined || value === null)
|
|
134
|
+
continue;
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
for (const item of value) {
|
|
137
|
+
if (item !== undefined && item !== null) {
|
|
138
|
+
qs.append(key, String(item));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
qs.set(key, String(value));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return qs;
|
|
147
|
+
}
|
|
148
|
+
// ── HttpClient ─────────────────────────────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Low-level HTTP client used by all domain resources.
|
|
151
|
+
*
|
|
152
|
+
* You should not need to instantiate this directly — use `FedPulse` instead.
|
|
153
|
+
*/
|
|
154
|
+
class HttpClient {
|
|
155
|
+
apiKey;
|
|
156
|
+
baseUrl;
|
|
157
|
+
timeoutMs;
|
|
158
|
+
maxRetries;
|
|
159
|
+
cache;
|
|
160
|
+
defaultCacheTtlMs;
|
|
161
|
+
fetchFn;
|
|
162
|
+
/** Most recent rate-limit info observed across all requests. */
|
|
163
|
+
lastRateLimit = null;
|
|
164
|
+
constructor(options) {
|
|
165
|
+
if (!options.apiKey || typeof options.apiKey !== 'string' || options.apiKey.trim() === '') {
|
|
166
|
+
throw new errors_js_1.AuthenticationError({
|
|
167
|
+
message: 'API key is required and must be a non-empty string. ' +
|
|
168
|
+
'Generate one at https://app.fedpulse.dev/dashboard.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
this.apiKey = options.apiKey.trim();
|
|
172
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
173
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
174
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
175
|
+
this.defaultCacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
176
|
+
this.fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
177
|
+
const cacheSize = options.cacheSize ?? DEFAULT_CACHE_SIZE;
|
|
178
|
+
this.cache = cacheSize > 0 ? new LruCache(cacheSize, this.defaultCacheTtlMs) : null;
|
|
179
|
+
}
|
|
180
|
+
// ── Public request methods ───────────────────────────────────────────────────
|
|
181
|
+
/**
|
|
182
|
+
* Perform a GET request. Results are cached unless `cacheTtlMs` is 0.
|
|
183
|
+
*
|
|
184
|
+
* @param path API path (e.g. `/v1/opportunities`).
|
|
185
|
+
* @param params Query parameters — arrays serialised as repeated keys.
|
|
186
|
+
* @param options Per-request overrides.
|
|
187
|
+
*/
|
|
188
|
+
async get(path, params = {}, options) {
|
|
189
|
+
const qs = buildQueryParams(params);
|
|
190
|
+
const qsStr = qs.toString();
|
|
191
|
+
const url = `${this.baseUrl}${path}${qsStr ? `?${qsStr}` : ''}`;
|
|
192
|
+
const ttl = options?.cacheTtlMs ?? this.defaultCacheTtlMs;
|
|
193
|
+
const shouldCache = this.cache !== null && ttl > 0;
|
|
194
|
+
if (shouldCache) {
|
|
195
|
+
const cached = this.cache.get(url);
|
|
196
|
+
if (cached !== undefined) {
|
|
197
|
+
return { ...cached, fromCache: true };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const result = await this.executeWithRetry('GET', url, undefined, options);
|
|
201
|
+
if (shouldCache) {
|
|
202
|
+
this.cache.set(url, result, ttl);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Perform a POST request (never cached).
|
|
208
|
+
*
|
|
209
|
+
* @param path API path.
|
|
210
|
+
* @param body JSON-serialisable request body.
|
|
211
|
+
* @param options Per-request overrides.
|
|
212
|
+
*/
|
|
213
|
+
async post(path, body, options) {
|
|
214
|
+
const url = `${this.baseUrl}${path}`;
|
|
215
|
+
return this.executeWithRetry('POST', url, body, options);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Perform a PATCH request (never cached).
|
|
219
|
+
*/
|
|
220
|
+
async patch(path, body, options) {
|
|
221
|
+
const url = `${this.baseUrl}${path}`;
|
|
222
|
+
return this.executeWithRetry('PATCH', url, body, options);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Perform a DELETE request (never cached).
|
|
226
|
+
*/
|
|
227
|
+
async del(path, options) {
|
|
228
|
+
const url = `${this.baseUrl}${path}`;
|
|
229
|
+
return this.executeWithRetry('DELETE', url, undefined, options);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Clear all cached responses.
|
|
233
|
+
*/
|
|
234
|
+
clearCache() {
|
|
235
|
+
this.cache?.clear();
|
|
236
|
+
}
|
|
237
|
+
// ── Internal methods ─────────────────────────────────────────────────────────
|
|
238
|
+
/**
|
|
239
|
+
* Execute a request, retrying on transient failures with exponential back-off
|
|
240
|
+
* and full jitter.
|
|
241
|
+
*/
|
|
242
|
+
async executeWithRetry(method, url, body, options) {
|
|
243
|
+
let lastError = null;
|
|
244
|
+
const maxAttempts = this.maxRetries + 1;
|
|
245
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
246
|
+
try {
|
|
247
|
+
return await this.executeOnce(method, url, body, options);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
if (!(err instanceof errors_js_1.FedPulseError)) {
|
|
251
|
+
// Should not happen, but guard defensively.
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
lastError = err;
|
|
255
|
+
// Do not retry 4xx errors (except 429).
|
|
256
|
+
const isRetryable = err instanceof errors_js_1.NetworkError ||
|
|
257
|
+
err instanceof errors_js_1.TimeoutError ||
|
|
258
|
+
RETRYABLE_STATUSES.has(err.status);
|
|
259
|
+
if (!isRetryable) {
|
|
260
|
+
// Non-retryable (4xx): throw the specific error type immediately.
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
if (attempt >= maxAttempts) {
|
|
264
|
+
// Retryable but no more attempts available.
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
// Calculate delay: exponential back-off (base 500ms) with full jitter.
|
|
268
|
+
let delayMs;
|
|
269
|
+
if (err instanceof errors_js_1.RateLimitError && err.retryAfter !== undefined) {
|
|
270
|
+
// Honour API-provided Retry-After header.
|
|
271
|
+
const waitUntil = err.retryAfter * 1000;
|
|
272
|
+
delayMs = Math.max(0, waitUntil - Date.now());
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const baseMs = 500 * Math.pow(2, attempt - 1); // 500, 1000, 2000 …
|
|
276
|
+
const capMs = 30_000;
|
|
277
|
+
delayMs = Math.random() * Math.min(baseMs, capMs);
|
|
278
|
+
}
|
|
279
|
+
await sleep(delayMs);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Only use RetryExhaustedError when we actually retried (maxAttempts > 1).
|
|
283
|
+
// With maxRetries=0 the caller gets the specific error type directly.
|
|
284
|
+
if (maxAttempts > 1) {
|
|
285
|
+
throw new errors_js_1.RetryExhaustedError({ lastError: lastError, attempts: maxAttempts });
|
|
286
|
+
}
|
|
287
|
+
throw lastError;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Execute a single HTTP request (no retry logic).
|
|
291
|
+
* Handles timeout, response parsing, header extraction, and error creation.
|
|
292
|
+
*/
|
|
293
|
+
async executeOnce(method, url, body, options) {
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
296
|
+
const headers = {
|
|
297
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
298
|
+
Accept: 'application/json',
|
|
299
|
+
'User-Agent': '@fedpulse/sdk/1.0.0 (node)',
|
|
300
|
+
...(options?.headers ?? {}),
|
|
301
|
+
};
|
|
302
|
+
if (body !== undefined && body !== null) {
|
|
303
|
+
headers['Content-Type'] = 'application/json';
|
|
304
|
+
}
|
|
305
|
+
let response;
|
|
306
|
+
try {
|
|
307
|
+
response = await this.fetchFn(url, {
|
|
308
|
+
method,
|
|
309
|
+
headers,
|
|
310
|
+
body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
|
|
311
|
+
signal: controller.signal,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
clearTimeout(timeoutId);
|
|
316
|
+
if (err != null && typeof err === 'object' && 'name' in err && err.name === 'AbortError') {
|
|
317
|
+
throw new errors_js_1.TimeoutError({
|
|
318
|
+
message: `Request timed out after ${this.timeoutMs}ms: ${method} ${url}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
322
|
+
throw new errors_js_1.NetworkError({
|
|
323
|
+
message: `Network error: ${cause.message}`,
|
|
324
|
+
cause,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
clearTimeout(timeoutId);
|
|
329
|
+
}
|
|
330
|
+
// Extract headers for rate limit tracking and error context.
|
|
331
|
+
const headerMap = {
|
|
332
|
+
'x-ratelimit-limit': response.headers.get('x-ratelimit-limit'),
|
|
333
|
+
'x-ratelimit-remaining': response.headers.get('x-ratelimit-remaining'),
|
|
334
|
+
'x-ratelimit-reset': response.headers.get('x-ratelimit-reset'),
|
|
335
|
+
'retry-after': response.headers.get('retry-after'),
|
|
336
|
+
};
|
|
337
|
+
const rateLimit = parseRateLimitHeaders(headerMap);
|
|
338
|
+
if (rateLimit !== null) {
|
|
339
|
+
this.lastRateLimit = rateLimit;
|
|
340
|
+
}
|
|
341
|
+
// Parse body — always attempt JSON; fall back to text for error messages.
|
|
342
|
+
let parsedBody;
|
|
343
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
344
|
+
if (contentType.includes('application/json')) {
|
|
345
|
+
try {
|
|
346
|
+
parsedBody = await response.json();
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
parsedBody = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Non-JSON body (e.g. download streams or plain-text errors).
|
|
354
|
+
parsedBody = null;
|
|
355
|
+
}
|
|
356
|
+
if (!response.ok) {
|
|
357
|
+
throw (0, errors_js_1.createApiError)(response.status, parsedBody, headerMap);
|
|
358
|
+
}
|
|
359
|
+
// Unwrap the standard success envelope.
|
|
360
|
+
const envelope = parsedBody;
|
|
361
|
+
if (envelope === null || typeof envelope !== 'object' || !('data' in envelope)) {
|
|
362
|
+
// Unexpected response shape — treat as server error.
|
|
363
|
+
throw (0, errors_js_1.createApiError)(500, { error: { message: 'Unexpected response format from API' } }, headerMap);
|
|
364
|
+
}
|
|
365
|
+
const meta = (envelope.meta ?? { requestId: '' });
|
|
366
|
+
return {
|
|
367
|
+
data: envelope.data,
|
|
368
|
+
pagination: envelope.pagination ?? null,
|
|
369
|
+
meta,
|
|
370
|
+
rateLimit,
|
|
371
|
+
fromCache: false,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Perform a raw fetch that returns the full Response (for streaming downloads).
|
|
376
|
+
* No retry, no envelope parsing.
|
|
377
|
+
*
|
|
378
|
+
* @param path API path.
|
|
379
|
+
* @param params Query parameters.
|
|
380
|
+
*/
|
|
381
|
+
async rawGet(path, params = {}) {
|
|
382
|
+
const qs = buildQueryParams(params);
|
|
383
|
+
const qsStr = qs.toString();
|
|
384
|
+
const url = `${this.baseUrl}${path}${qsStr ? `?${qsStr}` : ''}`;
|
|
385
|
+
const controller = new AbortController();
|
|
386
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
387
|
+
try {
|
|
388
|
+
const response = await this.fetchFn(url, {
|
|
389
|
+
method: 'GET',
|
|
390
|
+
headers: {
|
|
391
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
392
|
+
'User-Agent': '@fedpulse/sdk/1.0.0 (node)',
|
|
393
|
+
},
|
|
394
|
+
signal: controller.signal,
|
|
395
|
+
});
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
let body = null;
|
|
398
|
+
try {
|
|
399
|
+
body = await response.json();
|
|
400
|
+
}
|
|
401
|
+
catch { /* ignore */ }
|
|
402
|
+
throw (0, errors_js_1.createApiError)(response.status, body, {
|
|
403
|
+
'retry-after': response.headers.get('retry-after'),
|
|
404
|
+
'x-ratelimit-reset': response.headers.get('x-ratelimit-reset'),
|
|
405
|
+
'x-ratelimit-limit': response.headers.get('x-ratelimit-limit'),
|
|
406
|
+
'x-ratelimit-remaining': response.headers.get('x-ratelimit-remaining'),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return response;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
if (err instanceof errors_js_1.FedPulseError)
|
|
413
|
+
throw err;
|
|
414
|
+
if (err != null && typeof err === 'object' && 'name' in err && err.name === 'AbortError') {
|
|
415
|
+
throw new errors_js_1.TimeoutError({ message: `Request timed out after ${this.timeoutMs}ms` });
|
|
416
|
+
}
|
|
417
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
418
|
+
throw new errors_js_1.NetworkError({ message: `Network error: ${cause.message}`, cause });
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
clearTimeout(timeoutId);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
exports.HttpClient = HttpClient;
|
|
426
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
427
|
+
/**
|
|
428
|
+
* Parse X-RateLimit-* response headers into a typed object.
|
|
429
|
+
* Returns null if none of the headers are present.
|
|
430
|
+
*/
|
|
431
|
+
function parseRateLimitHeaders(headers) {
|
|
432
|
+
const limit = headers['x-ratelimit-limit'];
|
|
433
|
+
const remaining = headers['x-ratelimit-remaining'];
|
|
434
|
+
const reset = headers['x-ratelimit-reset'];
|
|
435
|
+
if (limit === null && remaining === null && reset === null)
|
|
436
|
+
return null;
|
|
437
|
+
return {
|
|
438
|
+
limit: limit !== null ? Number(limit) : 0,
|
|
439
|
+
remaining: remaining !== null ? Number(remaining) : 0,
|
|
440
|
+
reset: reset !== null ? Number(reset) : 0,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Promise-based sleep helper.
|
|
445
|
+
* @param ms Milliseconds to wait.
|
|
446
|
+
*/
|
|
447
|
+
function sleep(ms) {
|
|
448
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
449
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fedpulse/sdk — Official JavaScript/TypeScript SDK for the FedPulse API.
|
|
4
|
+
*
|
|
5
|
+
* Quick start:
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { FedPulse } from '@fedpulse/sdk';
|
|
8
|
+
*
|
|
9
|
+
* const client = new FedPulse({ apiKey: process.env.FEDPULSE_API_KEY! });
|
|
10
|
+
*
|
|
11
|
+
* const { data } = await client.opportunities.list({ q: 'cloud', naics: '541512' });
|
|
12
|
+
* console.log(data[0].title);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Webhook verification:
|
|
16
|
+
* ```ts
|
|
17
|
+
* const payload = FedPulse.verifyWebhook({
|
|
18
|
+
* rawBody: req.body,
|
|
19
|
+
* signatureHeader: req.headers['x-fedpulse-signature'],
|
|
20
|
+
* timestampHeader: req.headers['x-fedpulse-timestamp'],
|
|
21
|
+
* secret: process.env.FEDPULSE_WEBHOOK_SECRET!,
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.extractWebhookHeaders = exports.verifyWebhook = exports.WebhooksResource = exports.AnalyticsResource = exports.AssistanceResource = exports.IntelligenceResource = exports.EntitiesResource = exports.ExclusionsResource = exports.OpportunitiesResource = exports.buildQueryParams = exports.HttpClient = exports.RetryExhaustedError = exports.TimeoutError = exports.NetworkError = exports.ServerError = exports.RateLimitError = exports.ValidationError = exports.NotFoundError = exports.PermissionError = exports.AuthenticationError = exports.FedPulseError = exports.WebhookVerificationError = exports.FedPulse = void 0;
|
|
27
|
+
// ── Main client ────────────────────────────────────────────────────────────────
|
|
28
|
+
var client_js_1 = require("./client.cjs");
|
|
29
|
+
Object.defineProperty(exports, "FedPulse", { enumerable: true, get: function () { return client_js_1.FedPulse; } });
|
|
30
|
+
Object.defineProperty(exports, "WebhookVerificationError", { enumerable: true, get: function () { return client_js_1.WebhookVerificationError; } });
|
|
31
|
+
// ── Error classes ──────────────────────────────────────────────────────────────
|
|
32
|
+
var errors_js_1 = require("./errors.cjs");
|
|
33
|
+
Object.defineProperty(exports, "FedPulseError", { enumerable: true, get: function () { return errors_js_1.FedPulseError; } });
|
|
34
|
+
Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_js_1.AuthenticationError; } });
|
|
35
|
+
Object.defineProperty(exports, "PermissionError", { enumerable: true, get: function () { return errors_js_1.PermissionError; } });
|
|
36
|
+
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
|
|
37
|
+
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_js_1.ValidationError; } });
|
|
38
|
+
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_js_1.RateLimitError; } });
|
|
39
|
+
Object.defineProperty(exports, "ServerError", { enumerable: true, get: function () { return errors_js_1.ServerError; } });
|
|
40
|
+
Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return errors_js_1.NetworkError; } });
|
|
41
|
+
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
|
|
42
|
+
Object.defineProperty(exports, "RetryExhaustedError", { enumerable: true, get: function () { return errors_js_1.RetryExhaustedError; } });
|
|
43
|
+
// ── HTTP client (advanced usage) ───────────────────────────────────────────────
|
|
44
|
+
var http_js_1 = require("./http.cjs");
|
|
45
|
+
Object.defineProperty(exports, "HttpClient", { enumerable: true, get: function () { return http_js_1.HttpClient; } });
|
|
46
|
+
Object.defineProperty(exports, "buildQueryParams", { enumerable: true, get: function () { return http_js_1.buildQueryParams; } });
|
|
47
|
+
// ── Domain resources ───────────────────────────────────────────────────────────
|
|
48
|
+
var opportunities_js_1 = require("./resources/opportunities.cjs");
|
|
49
|
+
Object.defineProperty(exports, "OpportunitiesResource", { enumerable: true, get: function () { return opportunities_js_1.OpportunitiesResource; } });
|
|
50
|
+
var exclusions_js_1 = require("./resources/exclusions.cjs");
|
|
51
|
+
Object.defineProperty(exports, "ExclusionsResource", { enumerable: true, get: function () { return exclusions_js_1.ExclusionsResource; } });
|
|
52
|
+
var entities_js_1 = require("./resources/entities.cjs");
|
|
53
|
+
Object.defineProperty(exports, "EntitiesResource", { enumerable: true, get: function () { return entities_js_1.EntitiesResource; } });
|
|
54
|
+
var intelligence_js_1 = require("./resources/intelligence.cjs");
|
|
55
|
+
Object.defineProperty(exports, "IntelligenceResource", { enumerable: true, get: function () { return intelligence_js_1.IntelligenceResource; } });
|
|
56
|
+
var assistance_js_1 = require("./resources/assistance.cjs");
|
|
57
|
+
Object.defineProperty(exports, "AssistanceResource", { enumerable: true, get: function () { return assistance_js_1.AssistanceResource; } });
|
|
58
|
+
var analytics_js_1 = require("./resources/analytics.cjs");
|
|
59
|
+
Object.defineProperty(exports, "AnalyticsResource", { enumerable: true, get: function () { return analytics_js_1.AnalyticsResource; } });
|
|
60
|
+
var webhooks_js_1 = require("./resources/webhooks.cjs");
|
|
61
|
+
Object.defineProperty(exports, "WebhooksResource", { enumerable: true, get: function () { return webhooks_js_1.WebhooksResource; } });
|
|
62
|
+
// ── Webhook utilities ──────────────────────────────────────────────────────────
|
|
63
|
+
var webhooks_verify_js_1 = require("./webhooks-verify.cjs");
|
|
64
|
+
Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhooks_verify_js_1.verifyWebhook; } });
|
|
65
|
+
Object.defineProperty(exports, "extractWebhookHeaders", { enumerable: true, get: function () { return webhooks_verify_js_1.extractWebhookHeaders; } });
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Analytics resource client.
|
|
4
|
+
*
|
|
5
|
+
* Wraps all /v1/analytics endpoints (per-user API usage analytics):
|
|
6
|
+
* - summary GET /v1/analytics/summary
|
|
7
|
+
* - chart GET /v1/analytics/chart
|
|
8
|
+
* - endpoints GET /v1/analytics/endpoints
|
|
9
|
+
* - logs GET /v1/analytics/logs
|
|
10
|
+
* - logPages async generator over the logs endpoint
|
|
11
|
+
*
|
|
12
|
+
* All results are scoped to the authenticated user — no cross-user visibility.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AnalyticsResource = void 0;
|
|
16
|
+
class AnalyticsResource {
|
|
17
|
+
http;
|
|
18
|
+
constructor(http) {
|
|
19
|
+
this.http = http;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get KPI summary totals for the authenticated user.
|
|
23
|
+
*
|
|
24
|
+
* Returns: requests today, this month, in range, 4xx/5xx counts and rates.
|
|
25
|
+
* Analytics data is never cached — always returns live counts.
|
|
26
|
+
*
|
|
27
|
+
* @param params Optional key-scoping and time range.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const summary = await client.analytics.summary({ range: '7d' });
|
|
32
|
+
* console.log('Requests last 7d:', summary.data.requestsInRange);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
async summary(params = {}) {
|
|
36
|
+
validateAnalyticsParams(params);
|
|
37
|
+
return this.http.get('/v1/analytics/summary', params, { cacheTtlMs: 0 });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get per-day request counts broken down by success / 4xx / 5xx.
|
|
41
|
+
*
|
|
42
|
+
* @param params Optional key-scoping and time range.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const chart = await client.analytics.chart({ range: '30d' });
|
|
47
|
+
* for (const day of chart.data.days) {
|
|
48
|
+
* console.log(day.date, day.total);
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
async chart(params = {}) {
|
|
53
|
+
validateAnalyticsParams(params);
|
|
54
|
+
return this.http.get('/v1/analytics/chart', params, { cacheTtlMs: 0 });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get top-10 endpoints by request volume and by P95 latency.
|
|
58
|
+
*
|
|
59
|
+
* @param params Optional key-scoping and time range.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const endpoints = await client.analytics.endpoints({ range: '7d' });
|
|
64
|
+
* console.log('Slowest endpoint:', endpoints.data.byLatency[0]?.endpoint);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
async endpoints(params = {}) {
|
|
68
|
+
validateAnalyticsParams(params);
|
|
69
|
+
return this.http.get('/v1/analytics/endpoints', params, { cacheTtlMs: 0 });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get paginated API request logs for the authenticated user.
|
|
73
|
+
*
|
|
74
|
+
* Supports cursor-based pagination via the `cursor` parameter.
|
|
75
|
+
* Maximum `limit` is 1000.
|
|
76
|
+
*
|
|
77
|
+
* @param params Filter and pagination parameters.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const logs = await client.analytics.logs({ limit: 50, status: '4xx' });
|
|
82
|
+
* for (const entry of logs.data.items) {
|
|
83
|
+
* console.log(entry.method, entry.endpoint, entry.statusCode);
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
async logs(params = {}) {
|
|
88
|
+
if (params.limit !== undefined) {
|
|
89
|
+
if (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 1000) {
|
|
90
|
+
throw new Error('limit must be an integer between 1 and 1000');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
validateAnalyticsParams(params);
|
|
94
|
+
return this.http.get('/v1/analytics/logs', params, { cacheTtlMs: 0 });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Async generator that automatically paginates through all log entries.
|
|
98
|
+
*
|
|
99
|
+
* Uses cursor-based pagination. Yields one page at a time.
|
|
100
|
+
*
|
|
101
|
+
* @param params Same parameters as `logs()`. Do not pass `cursor`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* for await (const page of client.analytics.logPages({ range: '7d' })) {
|
|
106
|
+
* for (const entry of page.data.items) {
|
|
107
|
+
* console.log(entry.requestId, entry.statusCode);
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
async *logPages(params) {
|
|
113
|
+
let cursor = undefined;
|
|
114
|
+
let hasMore = true;
|
|
115
|
+
while (hasMore) {
|
|
116
|
+
const result = await this.logs({
|
|
117
|
+
...params,
|
|
118
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
119
|
+
});
|
|
120
|
+
yield result;
|
|
121
|
+
const logs = result.data;
|
|
122
|
+
cursor = logs.nextCursor ?? undefined;
|
|
123
|
+
hasMore = logs.hasNextPage && cursor !== undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.AnalyticsResource = AnalyticsResource;
|
|
128
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
129
|
+
const VALID_RANGES = new Set(['24h', '7d', '30d', '90d']);
|
|
130
|
+
function validateAnalyticsParams(params) {
|
|
131
|
+
if (params.range !== undefined && !VALID_RANGES.has(params.range)) {
|
|
132
|
+
throw new Error(`range must be one of: 24h, 7d, 30d, 90d. Got: "${params.range}"`);
|
|
133
|
+
}
|
|
134
|
+
}
|