@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.
- 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,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhooks resource client.
|
|
4
|
+
*
|
|
5
|
+
* Wraps all /v1/webhooks endpoints:
|
|
6
|
+
* - create POST /v1/webhooks
|
|
7
|
+
* - list GET /v1/webhooks
|
|
8
|
+
* - get GET /v1/webhooks/:id
|
|
9
|
+
* - update PATCH /v1/webhooks/:id
|
|
10
|
+
* - delete DELETE /v1/webhooks/:id
|
|
11
|
+
* - test POST /v1/webhooks/:id/test
|
|
12
|
+
* - resume POST /v1/webhooks/:id/resume
|
|
13
|
+
* - listDeliveries GET /v1/webhooks/:id/deliveries
|
|
14
|
+
* - getDelivery GET /v1/webhooks/:id/deliveries/:deliveryId
|
|
15
|
+
* - deliveryPages async generator over deliveries
|
|
16
|
+
*
|
|
17
|
+
* Requires: `webhook` API key scope for all operations.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.WebhooksResource = void 0;
|
|
21
|
+
class WebhooksResource {
|
|
22
|
+
http;
|
|
23
|
+
constructor(http) {
|
|
24
|
+
this.http = http;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a new webhook subscription.
|
|
28
|
+
*
|
|
29
|
+
* **Important:** The raw signing secret is returned once in the response
|
|
30
|
+
* (`result.data.secret`) and can never be retrieved again. Store it securely
|
|
31
|
+
* and use it to verify incoming deliveries with `FedPulse.verifyWebhook()`.
|
|
32
|
+
*
|
|
33
|
+
* @param params Webhook creation parameters.
|
|
34
|
+
* @throws {PermissionError} If the API key lacks the `webhook` scope.
|
|
35
|
+
* @throws {PermissionError} If the plan limit for webhooks has been reached.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const result = await client.webhooks.create({
|
|
40
|
+
* url: 'https://my-app.example.com/webhooks/fedpulse',
|
|
41
|
+
* events: ['opportunity.new', 'opportunity.modified'],
|
|
42
|
+
* filters: { naics: ['541512'], state: 'VA' },
|
|
43
|
+
* });
|
|
44
|
+
* const signingSecret = result.data.secret; // Store this securely!
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
async create(params) {
|
|
48
|
+
validateCreateParams(params);
|
|
49
|
+
return this.http.post('/v1/webhooks', params);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List all webhook subscriptions for the authenticated user.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const webhooks = await client.webhooks.list();
|
|
57
|
+
* for (const wh of webhooks.data) {
|
|
58
|
+
* console.log(wh.id, wh.url, wh.isActive);
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
async list() {
|
|
63
|
+
return this.http.get('/v1/webhooks', {}, { cacheTtlMs: 0 });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get a single webhook subscription.
|
|
67
|
+
*
|
|
68
|
+
* @param id Webhook UUID.
|
|
69
|
+
* @throws {NotFoundError} If the webhook does not exist or belongs to another user.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const wh = await client.webhooks.get('wh-uuid');
|
|
74
|
+
* console.log(wh.data.isActive, wh.data.consecutiveFailures);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
async get(id) {
|
|
78
|
+
validateId(id, 'id');
|
|
79
|
+
return this.http.get(`/v1/webhooks/${encodeURIComponent(id)}`, {}, { cacheTtlMs: 0 });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Update a webhook subscription.
|
|
83
|
+
*
|
|
84
|
+
* Pass `rotateSecret: true` to generate a new signing secret.
|
|
85
|
+
* The new secret will be returned in the response.
|
|
86
|
+
*
|
|
87
|
+
* @param id Webhook UUID.
|
|
88
|
+
* @param params Fields to update. At least one non-`rotateSecret` field required.
|
|
89
|
+
* @throws {NotFoundError} If the webhook does not exist.
|
|
90
|
+
* @throws {ValidationError} If no update fields are provided.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const updated = await client.webhooks.update('wh-uuid', {
|
|
95
|
+
* url: 'https://new-endpoint.example.com/webhooks',
|
|
96
|
+
* events: ['opportunity.new'],
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
async update(id, params) {
|
|
101
|
+
validateId(id, 'id');
|
|
102
|
+
if (Object.keys(params).length === 0) {
|
|
103
|
+
throw new Error('At least one field must be provided for update');
|
|
104
|
+
}
|
|
105
|
+
return this.http.patch(`/v1/webhooks/${encodeURIComponent(id)}`, params);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Delete a webhook subscription.
|
|
109
|
+
*
|
|
110
|
+
* @param id Webhook UUID.
|
|
111
|
+
* @throws {NotFoundError} If the webhook does not exist.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* await client.webhooks.delete('wh-uuid');
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
async delete(id) {
|
|
119
|
+
validateId(id, 'id');
|
|
120
|
+
return this.http.del(`/v1/webhooks/${encodeURIComponent(id)}`);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Send a test event to a webhook endpoint (synchronous delivery).
|
|
124
|
+
*
|
|
125
|
+
* Unlike real deliveries, test events are sent immediately (no queue) and
|
|
126
|
+
* do not count against the circuit-breaker failure counter.
|
|
127
|
+
*
|
|
128
|
+
* @param id Webhook UUID.
|
|
129
|
+
* @throws {NotFoundError} If the webhook does not exist.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const result = await client.webhooks.test('wh-uuid');
|
|
134
|
+
* console.log('Test delivery status:', result.data);
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
async test(id) {
|
|
138
|
+
validateId(id, 'id');
|
|
139
|
+
return this.http.post(`/v1/webhooks/${encodeURIComponent(id)}/test`, {});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resume a paused webhook (clear the circuit-breaker).
|
|
143
|
+
*
|
|
144
|
+
* Webhooks are auto-paused after 5 consecutive delivery failures.
|
|
145
|
+
* Use this to re-enable delivery after fixing the endpoint.
|
|
146
|
+
*
|
|
147
|
+
* @param id Webhook UUID.
|
|
148
|
+
* @throws {NotFoundError} If the webhook does not exist.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* await client.webhooks.resume('wh-uuid');
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
async resume(id) {
|
|
156
|
+
validateId(id, 'id');
|
|
157
|
+
return this.http.post(`/v1/webhooks/${encodeURIComponent(id)}/resume`, {});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get paginated delivery history for a webhook.
|
|
161
|
+
*
|
|
162
|
+
* @param id Webhook UUID.
|
|
163
|
+
* @param params Filter and pagination parameters.
|
|
164
|
+
* @throws {NotFoundError} If the webhook does not exist.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* const deliveries = await client.webhooks.listDeliveries('wh-uuid', {
|
|
169
|
+
* status: 'failed',
|
|
170
|
+
* limit: 50,
|
|
171
|
+
* });
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
async listDeliveries(id, params = {}) {
|
|
175
|
+
validateId(id, 'id');
|
|
176
|
+
return this.http.get(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, params, { cacheTtlMs: 0 });
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the full detail of a single webhook delivery, including the payload.
|
|
180
|
+
*
|
|
181
|
+
* @param id Webhook UUID.
|
|
182
|
+
* @param deliveryId Delivery UUID.
|
|
183
|
+
* @throws {NotFoundError} If the webhook or delivery does not exist.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* const delivery = await client.webhooks.getDelivery('wh-uuid', 'del-uuid');
|
|
188
|
+
* console.log('Payload:', delivery.data.payload);
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
async getDelivery(id, deliveryId) {
|
|
192
|
+
validateId(id, 'id');
|
|
193
|
+
validateId(deliveryId, 'deliveryId');
|
|
194
|
+
return this.http.get(`/v1/webhooks/${encodeURIComponent(id)}/deliveries/${encodeURIComponent(deliveryId)}`, {}, { cacheTtlMs: 0 });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Async generator that automatically paginates through delivery history.
|
|
198
|
+
*
|
|
199
|
+
* @param id Webhook UUID.
|
|
200
|
+
* @param params Same filter parameters as `listDeliveries()`. Do not pass `page`.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* for await (const page of client.webhooks.deliveryPages('wh-uuid', { status: 'failed' })) {
|
|
205
|
+
* for (const delivery of page.data) {
|
|
206
|
+
* console.log(delivery.id, delivery.status, delivery.httpStatus);
|
|
207
|
+
* }
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
async *deliveryPages(id, params = {}) {
|
|
212
|
+
validateId(id, 'id');
|
|
213
|
+
let page = 1;
|
|
214
|
+
let hasMore = true;
|
|
215
|
+
while (hasMore) {
|
|
216
|
+
const result = await this.listDeliveries(id, { ...params, page });
|
|
217
|
+
yield result;
|
|
218
|
+
const pagination = result.pagination;
|
|
219
|
+
if (pagination === null) {
|
|
220
|
+
hasMore = false;
|
|
221
|
+
}
|
|
222
|
+
else if ('hasNextPage' in pagination) {
|
|
223
|
+
hasMore = pagination.hasNextPage;
|
|
224
|
+
if (hasMore)
|
|
225
|
+
page++;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
hasMore = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.WebhooksResource = WebhooksResource;
|
|
234
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
235
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
236
|
+
function validateId(value, name) {
|
|
237
|
+
if (!value || typeof value !== 'string' || value.trim() === '') {
|
|
238
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
239
|
+
}
|
|
240
|
+
if (!UUID_RE.test(value.trim())) {
|
|
241
|
+
throw new Error(`${name} must be a valid UUID`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function validateCreateParams(params) {
|
|
245
|
+
if (!params.url || typeof params.url !== 'string' || params.url.trim() === '') {
|
|
246
|
+
throw new Error('url must be a non-empty string');
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const parsed = new URL(params.url);
|
|
250
|
+
if (parsed.protocol !== 'https:') {
|
|
251
|
+
throw new Error('url must use the https:// scheme');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (err instanceof Error && err.message.includes('https://'))
|
|
256
|
+
throw err;
|
|
257
|
+
throw new Error(`url is not a valid URL: ${params.url}`);
|
|
258
|
+
}
|
|
259
|
+
if (!Array.isArray(params.events) || params.events.length === 0) {
|
|
260
|
+
throw new Error('events must be a non-empty array of webhook event types');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook signature verification for FedPulse webhook deliveries.
|
|
4
|
+
*
|
|
5
|
+
* FedPulse signs every outgoing webhook POST with HMAC-SHA256.
|
|
6
|
+
* Use these utilities to verify that incoming requests are genuinely from
|
|
7
|
+
* FedPulse and have not been tampered with or replayed.
|
|
8
|
+
*
|
|
9
|
+
* ## Signing algorithm:
|
|
10
|
+
* signed_body = `${timestampSeconds}.${rawPayloadJson}`
|
|
11
|
+
* signature = HMAC-SHA256(rawSecret, signed_body) → hex
|
|
12
|
+
*
|
|
13
|
+
* ## Delivery headers (all present on every POST):
|
|
14
|
+
* X-FedPulse-Signature : `sha256={hex_hmac}`
|
|
15
|
+
* X-FedPulse-Timestamp : Unix epoch seconds (string)
|
|
16
|
+
* X-FedPulse-Event : Event type (e.g. "opportunity.new")
|
|
17
|
+
* X-FedPulse-Delivery-Id : UUIDv4 delivery identifier
|
|
18
|
+
*
|
|
19
|
+
* ## Replay protection:
|
|
20
|
+
* Reject deliveries where |now − timestamp| > maxAgeSeconds (default 300s).
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.WebhookVerificationError = void 0;
|
|
24
|
+
exports.verifyWebhook = verifyWebhook;
|
|
25
|
+
exports.extractWebhookHeaders = extractWebhookHeaders;
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
const errors_js_1 = require("./errors.cjs");
|
|
28
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
|
+
const DEFAULT_MAX_AGE_SECONDS = 300; // 5 minutes — matches API server policy.
|
|
30
|
+
// ── Error types ────────────────────────────────────────────────────────────────
|
|
31
|
+
/** Thrown when webhook signature verification fails. */
|
|
32
|
+
class WebhookVerificationError extends errors_js_1.FedPulseError {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super({ message, status: 0, code: 'WEBHOOK_VERIFICATION_FAILED' });
|
|
35
|
+
this.name = 'WebhookVerificationError';
|
|
36
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
40
|
+
// ── Public API ─────────────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Verify a FedPulse webhook delivery and parse the payload.
|
|
43
|
+
*
|
|
44
|
+
* This function performs three checks:
|
|
45
|
+
* 1. **Format check** — headers are present and in the expected format.
|
|
46
|
+
* 2. **Timestamp check** — delivery is not older than `maxAgeSeconds`.
|
|
47
|
+
* 3. **Signature check** — HMAC-SHA256 matches using constant-time comparison.
|
|
48
|
+
*
|
|
49
|
+
* Throws `WebhookVerificationError` on any failure.
|
|
50
|
+
* Returns the parsed, verified `WebhookPayload<T>`.
|
|
51
|
+
*
|
|
52
|
+
* @param input Headers, raw body, and secret.
|
|
53
|
+
* @returns Parsed and verified webhook payload.
|
|
54
|
+
*
|
|
55
|
+
* @throws {WebhookVerificationError} If the signature is invalid, the timestamp
|
|
56
|
+
* is out of range, or the headers are malformed.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* // Express.js example (use bodyParser.raw() to get the raw buffer):
|
|
61
|
+
* app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
|
|
62
|
+
* let payload;
|
|
63
|
+
* try {
|
|
64
|
+
* payload = FedPulse.verifyWebhook({
|
|
65
|
+
* rawBody: req.body,
|
|
66
|
+
* signatureHeader: req.headers['x-fedpulse-signature'] as string,
|
|
67
|
+
* timestampHeader: req.headers['x-fedpulse-timestamp'] as string,
|
|
68
|
+
* secret: process.env.FEDPULSE_WEBHOOK_SECRET!,
|
|
69
|
+
* });
|
|
70
|
+
* } catch (err) {
|
|
71
|
+
* return res.status(400).send('Invalid signature');
|
|
72
|
+
* }
|
|
73
|
+
* console.log('Event:', payload.event, 'Data:', payload.data);
|
|
74
|
+
* res.status(200).send('OK');
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function verifyWebhook(input) {
|
|
79
|
+
const { rawBody, signatureHeader, timestampHeader, secret, options } = input;
|
|
80
|
+
const maxAgeSeconds = options?.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
|
|
81
|
+
// ── 1. Validate input presence ─────────────────────────────────────────────
|
|
82
|
+
if (!signatureHeader || typeof signatureHeader !== 'string') {
|
|
83
|
+
throw new WebhookVerificationError('Missing X-FedPulse-Signature header. Ensure the request is from FedPulse.');
|
|
84
|
+
}
|
|
85
|
+
if (!timestampHeader || typeof timestampHeader !== 'string') {
|
|
86
|
+
throw new WebhookVerificationError('Missing X-FedPulse-Timestamp header. Ensure the request is from FedPulse.');
|
|
87
|
+
}
|
|
88
|
+
if (!secret || typeof secret !== 'string' || secret.trim() === '') {
|
|
89
|
+
throw new WebhookVerificationError('Webhook secret must be a non-empty string.');
|
|
90
|
+
}
|
|
91
|
+
if (rawBody === undefined || rawBody === null) {
|
|
92
|
+
throw new WebhookVerificationError('rawBody is required. Pass the exact bytes received from the HTTP request.');
|
|
93
|
+
}
|
|
94
|
+
// ── 2. Parse and validate timestamp ────────────────────────────────────────
|
|
95
|
+
const timestampSeconds = Number(timestampHeader);
|
|
96
|
+
if (!Number.isFinite(timestampSeconds) || timestampSeconds <= 0) {
|
|
97
|
+
throw new WebhookVerificationError(`X-FedPulse-Timestamp is not a valid Unix timestamp: "${timestampHeader}"`);
|
|
98
|
+
}
|
|
99
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
100
|
+
const ageSeconds = Math.abs(nowSeconds - timestampSeconds);
|
|
101
|
+
if (ageSeconds > maxAgeSeconds) {
|
|
102
|
+
throw new WebhookVerificationError(`Webhook delivery is too old (age: ${ageSeconds}s, max: ${maxAgeSeconds}s). ` +
|
|
103
|
+
'This may indicate a replay attack. Ensure your server clock is synchronised.');
|
|
104
|
+
}
|
|
105
|
+
// ── 3. Parse and validate signature header ─────────────────────────────────
|
|
106
|
+
if (!signatureHeader.startsWith('sha256=')) {
|
|
107
|
+
throw new WebhookVerificationError(`Unsupported signature algorithm. Expected "sha256=..." prefix, got: "${signatureHeader}"`);
|
|
108
|
+
}
|
|
109
|
+
const receivedHex = signatureHeader.slice('sha256='.length);
|
|
110
|
+
if (!receivedHex || !/^[0-9a-f]+$/i.test(receivedHex)) {
|
|
111
|
+
throw new WebhookVerificationError('X-FedPulse-Signature contains an invalid hex string');
|
|
112
|
+
}
|
|
113
|
+
// ── 4. Compute expected signature ──────────────────────────────────────────
|
|
114
|
+
const bodyString = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');
|
|
115
|
+
const signedPayload = `${timestampSeconds}.${bodyString}`;
|
|
116
|
+
const expectedHex = (0, node_crypto_1.createHmac)('sha256', secret)
|
|
117
|
+
.update(signedPayload, 'utf8')
|
|
118
|
+
.digest('hex');
|
|
119
|
+
// ── 5. Constant-time comparison ────────────────────────────────────────────
|
|
120
|
+
// Pad both to the same length to avoid length-based timing differences, then
|
|
121
|
+
// use timingSafeEqual to prevent timing attacks.
|
|
122
|
+
const receivedBuf = Buffer.from(receivedHex.toLowerCase(), 'hex');
|
|
123
|
+
const expectedBuf = Buffer.from(expectedHex, 'hex');
|
|
124
|
+
if (receivedBuf.length !== expectedBuf.length ||
|
|
125
|
+
!(0, node_crypto_1.timingSafeEqual)(receivedBuf, expectedBuf)) {
|
|
126
|
+
throw new WebhookVerificationError('Webhook signature does not match. The request may have been tampered with.');
|
|
127
|
+
}
|
|
128
|
+
// ── 6. Parse and return payload ────────────────────────────────────────────
|
|
129
|
+
let payload;
|
|
130
|
+
try {
|
|
131
|
+
payload = JSON.parse(bodyString);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new WebhookVerificationError('Webhook body is not valid JSON despite a valid signature.');
|
|
135
|
+
}
|
|
136
|
+
if (payload === null ||
|
|
137
|
+
typeof payload !== 'object' ||
|
|
138
|
+
!('event' in payload) ||
|
|
139
|
+
!('timestamp' in payload) ||
|
|
140
|
+
!('data' in payload)) {
|
|
141
|
+
throw new WebhookVerificationError('Webhook payload does not match the expected envelope shape ' +
|
|
142
|
+
'{ event, timestamp, apiVersion, data }.');
|
|
143
|
+
}
|
|
144
|
+
return payload;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extract the FedPulse webhook headers from a plain headers object.
|
|
148
|
+
*
|
|
149
|
+
* Normalises both lowercase and original-casing variants so you don't
|
|
150
|
+
* have to worry about header casing differences between frameworks.
|
|
151
|
+
*
|
|
152
|
+
* @param headers A plain object or Map of request headers.
|
|
153
|
+
* @returns Extracted signature and timestamp values.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* // Works with Express, Fastify, Koa, Next.js API routes, etc.
|
|
158
|
+
* const { signatureHeader, timestampHeader } = extractWebhookHeaders(req.headers);
|
|
159
|
+
* const payload = FedPulse.verifyWebhook({ rawBody, signatureHeader, timestampHeader, secret });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
function extractWebhookHeaders(headers) {
|
|
163
|
+
const get = (key) => {
|
|
164
|
+
let value;
|
|
165
|
+
if (headers instanceof Headers) {
|
|
166
|
+
value = headers.get(key);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Case-insensitive lookup for plain objects.
|
|
170
|
+
const lowerKey = key.toLowerCase();
|
|
171
|
+
const entry = Object.entries(headers).find(([k]) => k.toLowerCase() === lowerKey);
|
|
172
|
+
value = entry?.[1];
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(value))
|
|
175
|
+
return value[0] ?? '';
|
|
176
|
+
return value ?? '';
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
signatureHeader: get('x-fedpulse-signature'),
|
|
180
|
+
timestampHeader: get('x-fedpulse-timestamp'),
|
|
181
|
+
event: get('x-fedpulse-event'),
|
|
182
|
+
deliveryId: get('x-fedpulse-delivery-id'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FedPulse SDK — main client class.
|
|
3
|
+
*
|
|
4
|
+
* Instantiate this class with your API key to access all FedPulse
|
|
5
|
+
* data resources and the webhook verification utility.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { FedPulse } from '@fedpulse/sdk';
|
|
10
|
+
*
|
|
11
|
+
* const client = new FedPulse({ apiKey: process.env.FEDPULSE_API_KEY! });
|
|
12
|
+
*
|
|
13
|
+
* // Search contracts
|
|
14
|
+
* const { data } = await client.opportunities.list({ q: 'cloud', naics: '541512' });
|
|
15
|
+
*
|
|
16
|
+
* // Compliance check
|
|
17
|
+
* const { data: status } = await client.exclusions.check({ entities: [{ uei: 'ABCDEF123456' }] });
|
|
18
|
+
*
|
|
19
|
+
* // Verify an incoming webhook
|
|
20
|
+
* const payload = FedPulse.verifyWebhook({ rawBody, signatureHeader, timestampHeader, secret });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { HttpClient } from './http.js';
|
|
24
|
+
import { OpportunitiesResource } from './resources/opportunities.js';
|
|
25
|
+
import { ExclusionsResource } from './resources/exclusions.js';
|
|
26
|
+
import { EntitiesResource } from './resources/entities.js';
|
|
27
|
+
import { IntelligenceResource } from './resources/intelligence.js';
|
|
28
|
+
import { AssistanceResource } from './resources/assistance.js';
|
|
29
|
+
import { AnalyticsResource } from './resources/analytics.js';
|
|
30
|
+
import { WebhooksResource } from './resources/webhooks.js';
|
|
31
|
+
import { verifyWebhook, extractWebhookHeaders, WebhookVerificationError, } from './webhooks-verify.js';
|
|
32
|
+
// ── Main client class ──────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* The FedPulse SDK client.
|
|
35
|
+
*
|
|
36
|
+
* All API interactions go through the resource properties on this class.
|
|
37
|
+
* The static `verifyWebhook` method can be used independently of a client instance.
|
|
38
|
+
*/
|
|
39
|
+
export class FedPulse {
|
|
40
|
+
/** Low-level HTTP client (exposed for advanced usage only). */
|
|
41
|
+
http;
|
|
42
|
+
/** Federal contract opportunities (/v1/opportunities). */
|
|
43
|
+
opportunities;
|
|
44
|
+
/** SAM.gov exclusions and bulk compliance checks (/v1/exclusions). */
|
|
45
|
+
exclusions;
|
|
46
|
+
/** SAM.gov registered entities / vendors (/v1/entities). */
|
|
47
|
+
entities;
|
|
48
|
+
/** 360° entity intelligence and market analysis (/v1/intelligence). */
|
|
49
|
+
intelligence;
|
|
50
|
+
/** Federal assistance listings / CFDA programs (/v1/assistance). */
|
|
51
|
+
assistance;
|
|
52
|
+
/** Per-user API usage analytics (/v1/analytics). */
|
|
53
|
+
analytics;
|
|
54
|
+
/** Webhook subscription management (/v1/webhooks). */
|
|
55
|
+
webhooks;
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.http = new HttpClient(options);
|
|
58
|
+
this.opportunities = new OpportunitiesResource(this.http);
|
|
59
|
+
this.exclusions = new ExclusionsResource(this.http);
|
|
60
|
+
this.entities = new EntitiesResource(this.http);
|
|
61
|
+
this.intelligence = new IntelligenceResource(this.http);
|
|
62
|
+
this.assistance = new AssistanceResource(this.http);
|
|
63
|
+
this.analytics = new AnalyticsResource(this.http);
|
|
64
|
+
this.webhooks = new WebhooksResource(this.http);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Most recent rate-limit info observed from API responses.
|
|
68
|
+
*
|
|
69
|
+
* Updated after every API call. Useful for monitoring your rate-limit usage.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* await client.opportunities.list({ limit: 25 });
|
|
74
|
+
* console.log('Remaining requests:', client.rateLimit?.remaining);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
get rateLimit() {
|
|
78
|
+
return this.http.lastRateLimit;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Clear the in-memory response cache.
|
|
82
|
+
*
|
|
83
|
+
* Useful after writes that may invalidate cached GET responses.
|
|
84
|
+
*/
|
|
85
|
+
clearCache() {
|
|
86
|
+
this.http.clearCache();
|
|
87
|
+
}
|
|
88
|
+
// ── Static webhook utilities ───────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Verify an incoming FedPulse webhook delivery.
|
|
91
|
+
*
|
|
92
|
+
* Validates the HMAC-SHA256 signature, checks the timestamp against replay
|
|
93
|
+
* attacks, and returns the parsed payload on success.
|
|
94
|
+
*
|
|
95
|
+
* **IMPORTANT:** Pass the raw request body bytes — do not parse to JSON first.
|
|
96
|
+
*
|
|
97
|
+
* @param input Headers, raw body, and signing secret.
|
|
98
|
+
* @returns Parsed, verified webhook payload.
|
|
99
|
+
* @throws {WebhookVerificationError} If signature/timestamp is invalid.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* // Express.js with `express.raw({ type: 'application/json' })`:
|
|
104
|
+
* const payload = FedPulse.verifyWebhook<{ noticeId: string }>({
|
|
105
|
+
* rawBody: req.body, // Buffer from express.raw()
|
|
106
|
+
* signatureHeader: req.headers['x-fedpulse-signature'] as string,
|
|
107
|
+
* timestampHeader: req.headers['x-fedpulse-timestamp'] as string,
|
|
108
|
+
* secret: process.env.FEDPULSE_WEBHOOK_SECRET!,
|
|
109
|
+
* });
|
|
110
|
+
* console.log(payload.event, payload.data.noticeId);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
static verifyWebhook(input) {
|
|
114
|
+
return verifyWebhook(input);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Extract FedPulse webhook headers from a request headers object.
|
|
118
|
+
*
|
|
119
|
+
* Handles case-insensitive lookup across Express, Fastify, Next.js, etc.
|
|
120
|
+
*
|
|
121
|
+
* @param headers Headers object (plain object or `Headers` instance).
|
|
122
|
+
* @returns Signature header, timestamp header, event type, and delivery ID.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const { signatureHeader, timestampHeader } = FedPulse.extractWebhookHeaders(req.headers);
|
|
127
|
+
* const payload = FedPulse.verifyWebhook({ rawBody, signatureHeader, timestampHeader, secret });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
static extractWebhookHeaders(headers) {
|
|
131
|
+
return extractWebhookHeaders(headers);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export { WebhookVerificationError };
|
|
135
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAyB9B,kFAAkF;AAElF;;;;;GAKG;AACH,MAAM,OAAO,QAAQ;IACnB,+DAA+D;IACtD,IAAI,CAAa;IAE1B,0DAA0D;IACjD,aAAa,CAAwB;IAE9C,sEAAsE;IAC7D,UAAU,CAAqB;IAExC,4DAA4D;IACnD,QAAQ,CAAmB;IAEpC,uEAAuE;IAC9D,YAAY,CAAuB;IAE5C,oEAAoE;IAC3D,UAAU,CAAqB;IAExC,oDAAoD;IAC3C,SAAS,CAAoB;IAEtC,sDAAsD;IAC7C,QAAQ,CAAmB;IAEpC,YAAY,OAAwB;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;QAEpC,IAAI,CAAC,aAAa,GAAG,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,CAAC,UAAU,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChD,IAAI,CAAC,YAAY,GAAG,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,CAAC,UAAU,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,SAAS,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;;;;;OAUG;IACH,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;IACjC,CAAC;IAED;;;;OAIG;IACH,UAAU;QACR,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;IACzB,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,CAAC,aAAa,CAAc,KAAyB;QACzD,OAAO,aAAa,CAAI,KAAK,CAAC,CAAC;IACjC,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,qBAAqB,CAC1B,OAAgE;QAEhE,OAAO,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;CACF;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
|