@xenterprises/fastify-xhubspot 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/CHANGELOG.md +295 -0
- package/LICENSE +15 -0
- package/SECURITY.md +1078 -0
- package/index.d.ts +829 -0
- package/package.json +85 -0
- package/src/index.js +3 -0
- package/src/services/companies.js +138 -0
- package/src/services/contacts.js +327 -0
- package/src/services/customObjects.js +51 -0
- package/src/services/deals.js +52 -0
- package/src/services/engagement.js +54 -0
- package/src/xHubspot.js +57 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
# Security Guidelines for xHubspot
|
|
2
|
+
|
|
3
|
+
This document provides comprehensive security guidance for using the xHubspot Fastify plugin with HubSpot's CRM API. Follow these guidelines to protect your data and ensure secure integration.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [API Key Management](#api-key-management)
|
|
8
|
+
2. [Authentication & Authorization](#authentication--authorization)
|
|
9
|
+
3. [Data Protection & Privacy](#data-protection--privacy)
|
|
10
|
+
4. [Webhook Security](#webhook-security)
|
|
11
|
+
5. [Request Validation & Sanitization](#request-validation--sanitization)
|
|
12
|
+
6. [Error Handling & Logging](#error-handling--logging)
|
|
13
|
+
7. [Rate Limiting & DOS Protection](#rate-limiting--dos-protection)
|
|
14
|
+
8. [Network Security](#network-security)
|
|
15
|
+
9. [Third-Party Portal Integration](#third-party-portal-integration)
|
|
16
|
+
10. [Compliance & Regulations](#compliance--regulations)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. API Key Management
|
|
21
|
+
|
|
22
|
+
### 1.1 Token Generation & Storage
|
|
23
|
+
|
|
24
|
+
**DO:**
|
|
25
|
+
- Create Private App Access Tokens with minimal required scopes
|
|
26
|
+
- Store tokens in environment variables (never hardcode)
|
|
27
|
+
- Use `.env.local` for local development (never commit to version control)
|
|
28
|
+
- Rotate tokens every 90 days minimum
|
|
29
|
+
- Use different tokens for development, staging, and production
|
|
30
|
+
|
|
31
|
+
**DON'T:**
|
|
32
|
+
- Commit `.env` or token files to version control
|
|
33
|
+
- Share tokens via email, Slack, or chat
|
|
34
|
+
- Log tokens in application logs
|
|
35
|
+
- Use the same token across multiple environments
|
|
36
|
+
- Create tokens with all available scopes
|
|
37
|
+
|
|
38
|
+
### 1.2 Required OAuth Scopes
|
|
39
|
+
|
|
40
|
+
Only request scopes needed for your use case:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Minimum for contact management:
|
|
44
|
+
- crm.objects.contacts.read
|
|
45
|
+
- crm.objects.contacts.write
|
|
46
|
+
|
|
47
|
+
Minimum for company/deal management:
|
|
48
|
+
- crm.objects.companies.read
|
|
49
|
+
- crm.objects.companies.write
|
|
50
|
+
- crm.objects.deals.read
|
|
51
|
+
- crm.objects.deals.write
|
|
52
|
+
|
|
53
|
+
For custom objects:
|
|
54
|
+
- crm.objects.custom.read
|
|
55
|
+
- crm.objects.custom.write
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Avoid requesting:
|
|
59
|
+
- `crm.lists.read` / `crm.lists.write` - Only if absolutely necessary
|
|
60
|
+
- `crm.quotes.*` - Unless specifically needed
|
|
61
|
+
- `crm.pipelines.*` - Unless modifying pipelines
|
|
62
|
+
- `actions:*` - Administrative scope
|
|
63
|
+
|
|
64
|
+
### 1.3 Token Validation
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
// BAD: Token validation only on startup
|
|
68
|
+
async function xHubspot(fastify, options) {
|
|
69
|
+
const hubspot = new Client({ accessToken: options.apiKey });
|
|
70
|
+
// No token validation - fails silently
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// GOOD: Comprehensive token validation
|
|
74
|
+
async function xHubspot(fastify, options) {
|
|
75
|
+
const { apiKey, logRequests = false } = options;
|
|
76
|
+
|
|
77
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
78
|
+
throw new Error("HubSpot API Key is required and cannot be empty");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!apiKey.startsWith("pat-")) {
|
|
82
|
+
fastify.log.warn("⚠️ Provided API key does not match HubSpot Private App pattern");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hubspot = new Client({ accessToken: apiKey });
|
|
86
|
+
|
|
87
|
+
// Test token validity with a minimal read operation
|
|
88
|
+
try {
|
|
89
|
+
await hubspot.crm.contacts.basicApi.getPage({ limit: 1 });
|
|
90
|
+
fastify.log.info("✅ HubSpot API token validated successfully");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.status === 401) {
|
|
93
|
+
throw new Error("HubSpot API token is invalid or expired");
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 2. Authentication & Authorization
|
|
103
|
+
|
|
104
|
+
### 2.1 Access Control in Third-Party Portals
|
|
105
|
+
|
|
106
|
+
When integrating xHubspot with third-party portals:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
// BAD: No authorization checks
|
|
110
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
111
|
+
const contact = await fastify.contacts.create(request.body);
|
|
112
|
+
return contact;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// GOOD: Verify user authorization
|
|
116
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
117
|
+
// 1. Verify user authentication
|
|
118
|
+
if (!request.user) {
|
|
119
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 2. Verify user authorization for this operation
|
|
123
|
+
if (!request.user.permissions.includes("contacts:write")) {
|
|
124
|
+
return reply.status(403).send({ error: "Forbidden" });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Verify contact ownership/access
|
|
128
|
+
const contact = await fastify.contacts.create(request.body);
|
|
129
|
+
return contact;
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2.2 Role-Based Access Control (RBAC)
|
|
134
|
+
|
|
135
|
+
Implement granular permission checks:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const permissions = {
|
|
139
|
+
"contacts:read": ["getById", "getByEmail", "list", "search"],
|
|
140
|
+
"contacts:write": ["create", "update", "batchCreate", "batchUpdate"],
|
|
141
|
+
"contacts:delete": ["delete"],
|
|
142
|
+
"companies:read": ["getById", "list"],
|
|
143
|
+
"companies:write": ["create", "update"],
|
|
144
|
+
"engagement:write": ["createNote", "createTask", "createCall", "createEmail"],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Verify permission before allowing operation
|
|
148
|
+
async function checkPermission(user, service, method) {
|
|
149
|
+
const allowedServices = permissions[`${service}:write`] || [];
|
|
150
|
+
if (!allowedServices.includes(method)) {
|
|
151
|
+
throw new Error(`User lacks permission for ${service}.${method}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 2.3 Session Management
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// Use secure session configuration
|
|
160
|
+
fastify.register(require("@fastify/session"), {
|
|
161
|
+
secret: process.env.SESSION_SECRET,
|
|
162
|
+
cookie: {
|
|
163
|
+
secure: process.env.NODE_ENV === "production", // HTTPS only in production
|
|
164
|
+
httpOnly: true, // Prevent JavaScript access
|
|
165
|
+
sameSite: "strict", // CSRF protection
|
|
166
|
+
maxAge: 3600000, // 1 hour
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 3. Data Protection & Privacy
|
|
174
|
+
|
|
175
|
+
### 3.1 Sensitive Data Handling
|
|
176
|
+
|
|
177
|
+
**Sensitive fields in HubSpot:**
|
|
178
|
+
- Email addresses
|
|
179
|
+
- Phone numbers
|
|
180
|
+
- Social security numbers
|
|
181
|
+
- Payment card information
|
|
182
|
+
- Home addresses
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
// BAD: Logging sensitive data
|
|
186
|
+
if (logRequests) {
|
|
187
|
+
console.log("Creating contact:", contactData);
|
|
188
|
+
// Outputs: { email: 'john@example.com', phone: '+1234567890' }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// GOOD: Mask sensitive data before logging
|
|
192
|
+
function maskSensitiveData(data) {
|
|
193
|
+
const masked = { ...data };
|
|
194
|
+
if (masked.email) {
|
|
195
|
+
masked.email = masked.email.replace(/(.{2})(.*)(.{2})/, "$1****$3");
|
|
196
|
+
}
|
|
197
|
+
if (masked.phone) {
|
|
198
|
+
masked.phone = masked.phone.replace(/\d(?=\d{4})/g, "*");
|
|
199
|
+
}
|
|
200
|
+
return masked;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (logRequests) {
|
|
204
|
+
console.log("Creating contact:", maskSensitiveData(contactData));
|
|
205
|
+
// Outputs: { email: 'jo****om', phone: '****7890' }
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3.2 Encryption in Transit
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// HTTPS is mandatory for all production requests
|
|
213
|
+
const fastify = Fastify({
|
|
214
|
+
https: {
|
|
215
|
+
key: fs.readFileSync("./private-key.pem"),
|
|
216
|
+
cert: fs.readFileSync("./certificate.pem"),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Verify TLS version 1.2 minimum
|
|
221
|
+
// Node.js default is TLS 1.2+, but verify in headers
|
|
222
|
+
fastify.register(require("@fastify/helmet"), {
|
|
223
|
+
contentSecurityPolicy: false,
|
|
224
|
+
strictTransportSecurity: {
|
|
225
|
+
maxAge: 31536000, // 1 year
|
|
226
|
+
includeSubDomains: true,
|
|
227
|
+
preload: true,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 3.3 Data Minimization
|
|
233
|
+
|
|
234
|
+
Only retrieve properties you need:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
// BAD: Request all properties
|
|
238
|
+
const contact = await fastify.contacts.getById(contactId);
|
|
239
|
+
|
|
240
|
+
// GOOD: Request only necessary properties
|
|
241
|
+
const contact = await fastify.contacts.getById(contactId, [
|
|
242
|
+
"firstname",
|
|
243
|
+
"lastname",
|
|
244
|
+
"email",
|
|
245
|
+
"phone",
|
|
246
|
+
]);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 3.4 GDPR & Privacy Compliance
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
// Right to be forgotten - delete contact data
|
|
253
|
+
async function deleteContact(contactId) {
|
|
254
|
+
// 1. Log the deletion request for audit trail
|
|
255
|
+
auditLog.record({
|
|
256
|
+
action: "DELETE_CONTACT",
|
|
257
|
+
contactId,
|
|
258
|
+
timestamp: new Date(),
|
|
259
|
+
reason: "GDPR Right to Deletion",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// 2. Delete from HubSpot
|
|
263
|
+
await fastify.contacts.delete(contactId);
|
|
264
|
+
|
|
265
|
+
// 3. Delete from local cache/database
|
|
266
|
+
await db.contacts.delete(contactId);
|
|
267
|
+
|
|
268
|
+
// 4. Verify deletion
|
|
269
|
+
try {
|
|
270
|
+
await fastify.contacts.getById(contactId);
|
|
271
|
+
throw new Error("Contact deletion failed - data still exists");
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error.status === 404) {
|
|
274
|
+
// Expected - contact successfully deleted
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Right to access - provide contact data
|
|
280
|
+
async function getContactData(contactId) {
|
|
281
|
+
const contact = await fastify.contacts.getById(contactId, null);
|
|
282
|
+
const engagements = await fastify.engagement.getEngagements({
|
|
283
|
+
contactId,
|
|
284
|
+
});
|
|
285
|
+
const associations = await fastify.contacts.getAssociations(contactId);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
personalData: contact,
|
|
289
|
+
engagementHistory: engagements,
|
|
290
|
+
associations,
|
|
291
|
+
exportedAt: new Date().toISOString(),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 4. Webhook Security
|
|
299
|
+
|
|
300
|
+
### 4.1 Webhook Signature Validation
|
|
301
|
+
|
|
302
|
+
HubSpot signs webhooks with an HMAC-SHA256 signature. Always validate:
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
import crypto from "crypto";
|
|
306
|
+
|
|
307
|
+
function validateWebhookSignature(request, secret) {
|
|
308
|
+
const signature = request.headers["x-hubspot-request-signature"];
|
|
309
|
+
const timestamp = request.headers["x-hubspot-request-timestamp"];
|
|
310
|
+
|
|
311
|
+
if (!signature || !timestamp) {
|
|
312
|
+
throw new Error("Missing webhook signature or timestamp");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Prevent replay attacks - signature must be recent (within 5 minutes)
|
|
316
|
+
const webhookTime = parseInt(timestamp);
|
|
317
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
318
|
+
if (Math.abs(webhookTime - currentTime) > 300) {
|
|
319
|
+
throw new Error("Webhook timestamp too old - possible replay attack");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Reconstruct the signature
|
|
323
|
+
const stringToSign = `${timestamp}${JSON.stringify(request.body)}`;
|
|
324
|
+
const expectedSignature = crypto
|
|
325
|
+
.createHmac("sha256", secret)
|
|
326
|
+
.update(stringToSign)
|
|
327
|
+
.digest("hex");
|
|
328
|
+
|
|
329
|
+
// Constant-time comparison to prevent timing attacks
|
|
330
|
+
if (
|
|
331
|
+
!crypto.timingSafeEqual(
|
|
332
|
+
Buffer.from(signature),
|
|
333
|
+
Buffer.from(expectedSignature)
|
|
334
|
+
)
|
|
335
|
+
) {
|
|
336
|
+
throw new Error("Invalid webhook signature");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Usage in Fastify hook
|
|
343
|
+
fastify.post("/webhooks/hubspot", async (request, reply) => {
|
|
344
|
+
try {
|
|
345
|
+
validateWebhookSignature(
|
|
346
|
+
request,
|
|
347
|
+
process.env.HUBSPOT_WEBHOOK_SECRET
|
|
348
|
+
);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
fastify.log.error("Webhook validation failed:", error.message);
|
|
351
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Process webhook...
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 4.2 Webhook Event Filtering
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
// Only subscribe to necessary events
|
|
362
|
+
const ALLOWED_EVENTS = [
|
|
363
|
+
"contact.creation",
|
|
364
|
+
"contact.change",
|
|
365
|
+
"deal.creation",
|
|
366
|
+
"deal.change",
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
fastify.post("/webhooks/hubspot", async (request, reply) => {
|
|
370
|
+
const { eventType } = request.body;
|
|
371
|
+
|
|
372
|
+
// Reject unexpected events
|
|
373
|
+
if (!ALLOWED_EVENTS.includes(eventType)) {
|
|
374
|
+
fastify.log.warn(`Received unexpected event: ${eventType}`);
|
|
375
|
+
return reply.status(202).send({ received: true });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Process whitelisted events
|
|
379
|
+
await handleWebhookEvent(request.body);
|
|
380
|
+
reply.status(200).send({ success: true });
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### 4.3 Webhook Processing Best Practices
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
// BAD: Blocking webhook processing
|
|
388
|
+
fastify.post("/webhooks/hubspot", async (request, reply) => {
|
|
389
|
+
const result = await processWebhook(request.body); // Takes 30 seconds
|
|
390
|
+
reply.status(200).send(result);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// GOOD: Async webhook processing with immediate response
|
|
394
|
+
fastify.post("/webhooks/hubspot", async (request, reply) => {
|
|
395
|
+
// Immediately acknowledge webhook
|
|
396
|
+
reply.status(202).send({ accepted: true });
|
|
397
|
+
|
|
398
|
+
// Process asynchronously in background
|
|
399
|
+
processWebhookAsync(request.body)
|
|
400
|
+
.catch((error) => {
|
|
401
|
+
fastify.log.error("Webhook processing failed:", error);
|
|
402
|
+
// Log to error tracking service (Sentry, etc.)
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
async function processWebhookAsync(event) {
|
|
407
|
+
// Add to queue for processing
|
|
408
|
+
await webhookQueue.add(event);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 5. Request Validation & Sanitization
|
|
415
|
+
|
|
416
|
+
### 5.1 Input Validation
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
import { z } from "zod";
|
|
420
|
+
|
|
421
|
+
const ContactSchema = z.object({
|
|
422
|
+
email: z.string().email().optional(),
|
|
423
|
+
firstname: z.string().max(50).optional(),
|
|
424
|
+
lastname: z.string().max(50).optional(),
|
|
425
|
+
phone: z.string().regex(/^\+?[0-9\s\-()]+$/).optional(),
|
|
426
|
+
hs_lead_status: z.enum(["NEW", "OPEN", "IN_PROGRESS", "OPEN_DEAL"]).optional(),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Validate before creating contact
|
|
430
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
431
|
+
try {
|
|
432
|
+
const validatedData = ContactSchema.parse(request.body);
|
|
433
|
+
const contact = await fastify.contacts.create(validatedData);
|
|
434
|
+
return contact;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error instanceof z.ZodError) {
|
|
437
|
+
return reply.status(400).send({
|
|
438
|
+
error: "Validation failed",
|
|
439
|
+
details: error.errors,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### 5.2 Email Validation
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
import { z } from "zod";
|
|
451
|
+
|
|
452
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
453
|
+
const BLOCKED_DOMAINS = [
|
|
454
|
+
"test.com",
|
|
455
|
+
"example.com",
|
|
456
|
+
"localhost",
|
|
457
|
+
"invalid.com",
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
function validateEmail(email) {
|
|
461
|
+
if (!EMAIL_REGEX.test(email)) {
|
|
462
|
+
throw new Error("Invalid email format");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const [, domain] = email.split("@");
|
|
466
|
+
if (BLOCKED_DOMAINS.includes(domain.toLowerCase())) {
|
|
467
|
+
throw new Error("Email domain is blocked");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Usage
|
|
474
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
475
|
+
if (request.body.email) {
|
|
476
|
+
validateEmail(request.body.email);
|
|
477
|
+
}
|
|
478
|
+
const contact = await fastify.contacts.create(request.body);
|
|
479
|
+
return contact;
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 5.3 Phone Number Validation
|
|
484
|
+
|
|
485
|
+
```javascript
|
|
486
|
+
import libphonenumber from "libphonenumber-js";
|
|
487
|
+
|
|
488
|
+
function validatePhoneNumber(phone, defaultCountry = "US") {
|
|
489
|
+
try {
|
|
490
|
+
const number = libphonenumber(phone, defaultCountry);
|
|
491
|
+
if (!number || !number.isValid()) {
|
|
492
|
+
throw new Error("Invalid phone number");
|
|
493
|
+
}
|
|
494
|
+
return number.format("E.164"); // +1234567890
|
|
495
|
+
} catch (error) {
|
|
496
|
+
throw new Error("Phone number validation failed");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Usage
|
|
501
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
502
|
+
if (request.body.phone) {
|
|
503
|
+
request.body.phone = validatePhoneNumber(request.body.phone);
|
|
504
|
+
}
|
|
505
|
+
const contact = await fastify.contacts.create(request.body);
|
|
506
|
+
return contact;
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 5.4 SQL/NoSQL Injection Prevention
|
|
511
|
+
|
|
512
|
+
Use the HubSpot client library properly - never build queries manually:
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
// BAD: Manual query building (never do this)
|
|
516
|
+
const query = `contacts with email = '${userInput}'`;
|
|
517
|
+
|
|
518
|
+
// GOOD: Use library's built-in methods
|
|
519
|
+
const contact = await fastify.contacts.getByEmail(userInput);
|
|
520
|
+
|
|
521
|
+
// GOOD: Use proper filtering with the API
|
|
522
|
+
const contacts = await fastify.contacts.search("email", userInput);
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## 6. Error Handling & Logging
|
|
528
|
+
|
|
529
|
+
### 6.1 Secure Error Messages
|
|
530
|
+
|
|
531
|
+
```javascript
|
|
532
|
+
// BAD: Exposing internal error details
|
|
533
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
534
|
+
try {
|
|
535
|
+
const contact = await fastify.contacts.create(request.body);
|
|
536
|
+
return contact;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return reply.status(500).send({
|
|
539
|
+
error: error.message, // Exposes HubSpot API details!
|
|
540
|
+
stack: error.stack, // Exposes internal paths
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// GOOD: Generic user-facing errors with detailed internal logging
|
|
546
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
547
|
+
try {
|
|
548
|
+
const contact = await fastify.contacts.create(request.body);
|
|
549
|
+
return contact;
|
|
550
|
+
} catch (error) {
|
|
551
|
+
// Log full details for debugging
|
|
552
|
+
fastify.log.error({
|
|
553
|
+
message: "Contact creation failed",
|
|
554
|
+
error: error.message,
|
|
555
|
+
stack: error.stack,
|
|
556
|
+
correlationId: error.correlationId,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Return generic error to user
|
|
560
|
+
const status = error.status || 500;
|
|
561
|
+
return reply.status(status).send({
|
|
562
|
+
error: "An error occurred. Please contact support.",
|
|
563
|
+
correlationId: error.correlationId, // For support inquiries
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### 6.2 Structured Logging
|
|
570
|
+
|
|
571
|
+
```javascript
|
|
572
|
+
// Use structured logging for security events
|
|
573
|
+
function logSecurityEvent(fastify, event, details) {
|
|
574
|
+
fastify.log.info({
|
|
575
|
+
type: "SECURITY_EVENT",
|
|
576
|
+
event,
|
|
577
|
+
timestamp: new Date().toISOString(),
|
|
578
|
+
...details,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Usage
|
|
583
|
+
if (!request.user) {
|
|
584
|
+
logSecurityEvent(fastify, "UNAUTHORIZED_ACCESS", {
|
|
585
|
+
endpoint: request.url,
|
|
586
|
+
ip: request.ip,
|
|
587
|
+
headers: request.headers,
|
|
588
|
+
});
|
|
589
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!hasPermission) {
|
|
593
|
+
logSecurityEvent(fastify, "FORBIDDEN_ACCESS", {
|
|
594
|
+
userId: request.user.id,
|
|
595
|
+
endpoint: request.url,
|
|
596
|
+
requiredPermission,
|
|
597
|
+
userPermissions: request.user.permissions,
|
|
598
|
+
});
|
|
599
|
+
return reply.status(403).send({ error: "Forbidden" });
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 6.3 Never Log Sensitive Data
|
|
604
|
+
|
|
605
|
+
```javascript
|
|
606
|
+
// Configuration for xHubspot
|
|
607
|
+
const config = {
|
|
608
|
+
apiKey: process.env.HUBSPOT_ACCESS_TOKEN,
|
|
609
|
+
logRequests: process.env.HUBSPOT_LOG_REQUESTS === "true",
|
|
610
|
+
logSensitiveData: false, // ALWAYS false in production
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Sensitive fields to never log
|
|
614
|
+
const SENSITIVE_FIELDS = [
|
|
615
|
+
"email",
|
|
616
|
+
"phone",
|
|
617
|
+
"ssn",
|
|
618
|
+
"creditcard",
|
|
619
|
+
"bankaccount",
|
|
620
|
+
"password",
|
|
621
|
+
"apiKey",
|
|
622
|
+
"accessToken",
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
function sanitizeForLogging(data) {
|
|
626
|
+
const sanitized = JSON.parse(JSON.stringify(data));
|
|
627
|
+
|
|
628
|
+
function mask(obj) {
|
|
629
|
+
if (typeof obj !== "object" || obj === null) return;
|
|
630
|
+
for (const key in obj) {
|
|
631
|
+
if (SENSITIVE_FIELDS.some((field) => key.toLowerCase().includes(field))) {
|
|
632
|
+
obj[key] = "***REDACTED***";
|
|
633
|
+
} else if (typeof obj[key] === "object") {
|
|
634
|
+
mask(obj[key]);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
mask(sanitized);
|
|
640
|
+
return sanitized;
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## 7. Rate Limiting & DOS Protection
|
|
647
|
+
|
|
648
|
+
### 7.1 HubSpot API Rate Limiting
|
|
649
|
+
|
|
650
|
+
```javascript
|
|
651
|
+
// Configure rate limiting based on your HubSpot tier
|
|
652
|
+
const RATE_LIMITS = {
|
|
653
|
+
free: 10, // requests per second
|
|
654
|
+
professional: 100,
|
|
655
|
+
enterprise: 500,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Use the configured rate limit
|
|
659
|
+
const config = {
|
|
660
|
+
rateLimit: process.env.HUBSPOT_RATE_LIMIT || RATE_LIMITS.free,
|
|
661
|
+
maxRetries: parseInt(process.env.HUBSPOT_MAX_RETRIES || "3"),
|
|
662
|
+
retryDelay: parseInt(process.env.HUBSPOT_RETRY_DELAY || "1000"),
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// Implement backoff strategy for rate limit errors
|
|
666
|
+
async function withRetry(fn, maxRetries = 3) {
|
|
667
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
668
|
+
try {
|
|
669
|
+
return await fn();
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (error.status === 429) {
|
|
672
|
+
// Rate limited
|
|
673
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
674
|
+
fastify.log.warn(
|
|
675
|
+
`Rate limited. Retrying after ${delay}ms (attempt ${attempt}/${maxRetries})`
|
|
676
|
+
);
|
|
677
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
678
|
+
} else {
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 7.2 Application-Level Rate Limiting
|
|
687
|
+
|
|
688
|
+
```javascript
|
|
689
|
+
import fastifyRateLimit from "@fastify/rate-limit";
|
|
690
|
+
|
|
691
|
+
fastify.register(fastifyRateLimit, {
|
|
692
|
+
max: 100, // Max requests per window
|
|
693
|
+
timeWindow: "15 minutes",
|
|
694
|
+
cache: 10000, // Number of records to store
|
|
695
|
+
allowList: ["127.0.0.1"], // Whitelist IPs
|
|
696
|
+
redis: process.env.REDIS_URL, // Optional: use Redis for distributed rate limiting
|
|
697
|
+
});
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### 7.3 Batch Operation Limits
|
|
701
|
+
|
|
702
|
+
```javascript
|
|
703
|
+
// Enforce HubSpot's batch limits
|
|
704
|
+
const BATCH_SIZE_LIMITS = {
|
|
705
|
+
contacts: 100,
|
|
706
|
+
companies: 100,
|
|
707
|
+
deals: 100,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
fastify.post("/api/contacts/batch", async (request, reply) => {
|
|
711
|
+
const { contacts } = request.body;
|
|
712
|
+
|
|
713
|
+
if (!Array.isArray(contacts)) {
|
|
714
|
+
return reply.status(400).send({ error: "Expected array of contacts" });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (contacts.length > BATCH_SIZE_LIMITS.contacts) {
|
|
718
|
+
return reply.status(400).send({
|
|
719
|
+
error: `Batch size exceeds maximum of ${BATCH_SIZE_LIMITS.contacts}`,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const result = await fastify.contacts.batchCreate(contacts);
|
|
724
|
+
return result;
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## 8. Network Security
|
|
731
|
+
|
|
732
|
+
### 8.1 CORS Configuration
|
|
733
|
+
|
|
734
|
+
```javascript
|
|
735
|
+
import fastifyCors from "@fastify/cors";
|
|
736
|
+
|
|
737
|
+
fastify.register(fastifyCors, {
|
|
738
|
+
origin: process.env.ALLOWED_ORIGINS?.split(",") || ["https://app.example.com"],
|
|
739
|
+
credentials: true,
|
|
740
|
+
methods: ["GET", "POST", "PUT", "DELETE"],
|
|
741
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
742
|
+
});
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 8.2 HTTPS Enforcement
|
|
746
|
+
|
|
747
|
+
```javascript
|
|
748
|
+
fastify.register(require("@fastify/helmet"), {
|
|
749
|
+
strictTransportSecurity: {
|
|
750
|
+
maxAge: 31536000,
|
|
751
|
+
includeSubDomains: true,
|
|
752
|
+
preload: true,
|
|
753
|
+
},
|
|
754
|
+
frameguard: {
|
|
755
|
+
action: "deny", // Prevent clickjacking
|
|
756
|
+
},
|
|
757
|
+
noSniff: true,
|
|
758
|
+
xssFilter: true,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Redirect HTTP to HTTPS in production
|
|
762
|
+
if (process.env.NODE_ENV === "production") {
|
|
763
|
+
fastify.register(require("@fastify/https-redirect"));
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### 8.3 VPN/IP Whitelisting
|
|
768
|
+
|
|
769
|
+
```javascript
|
|
770
|
+
// Optional: Restrict API access to specific IPs
|
|
771
|
+
const ALLOWED_IPS = (process.env.HUBSPOT_ALLOWED_IPS || "").split(",").filter(Boolean);
|
|
772
|
+
|
|
773
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
774
|
+
if (ALLOWED_IPS.length > 0) {
|
|
775
|
+
const clientIP = request.ip;
|
|
776
|
+
if (!ALLOWED_IPS.includes(clientIP)) {
|
|
777
|
+
fastify.log.warn(`Access denied from IP: ${clientIP}`);
|
|
778
|
+
return reply.status(403).send({ error: "Forbidden" });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## 9. Third-Party Portal Integration
|
|
787
|
+
|
|
788
|
+
### 9.1 Portal Authentication
|
|
789
|
+
|
|
790
|
+
```javascript
|
|
791
|
+
// Implement JWT-based authentication for portal
|
|
792
|
+
import jwt from "@fastify/jwt";
|
|
793
|
+
|
|
794
|
+
fastify.register(jwt, {
|
|
795
|
+
secret: process.env.JWT_SECRET,
|
|
796
|
+
sign: { expiresIn: "24h" },
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Login endpoint
|
|
800
|
+
fastify.post("/auth/login", async (request, reply) => {
|
|
801
|
+
const { username, password } = request.body;
|
|
802
|
+
|
|
803
|
+
// Verify credentials (example with bcrypt)
|
|
804
|
+
const user = await verifyCredentials(username, password);
|
|
805
|
+
|
|
806
|
+
if (!user) {
|
|
807
|
+
return reply.status(401).send({ error: "Invalid credentials" });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const token = fastify.jwt.sign({
|
|
811
|
+
userId: user.id,
|
|
812
|
+
username: user.username,
|
|
813
|
+
permissions: user.permissions,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return { token };
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Protect routes with authentication
|
|
820
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
821
|
+
try {
|
|
822
|
+
await request.jwtVerify();
|
|
823
|
+
} catch (error) {
|
|
824
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const contact = await fastify.contacts.create(request.body);
|
|
828
|
+
return contact;
|
|
829
|
+
});
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### 9.2 Portal Data Isolation
|
|
833
|
+
|
|
834
|
+
```javascript
|
|
835
|
+
// Ensure portal users only access their own data
|
|
836
|
+
async function getPortalUserContacts(userId) {
|
|
837
|
+
// Get contacts associated with this portal user
|
|
838
|
+
const contacts = await db.contacts.find({ portalUserId: userId });
|
|
839
|
+
return contacts;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Verify access before returning data
|
|
843
|
+
fastify.get("/api/contacts/:id", async (request, reply) => {
|
|
844
|
+
const { id } = request.params;
|
|
845
|
+
const { userId } = request.user;
|
|
846
|
+
|
|
847
|
+
const contact = await fastify.contacts.getById(id);
|
|
848
|
+
|
|
849
|
+
// Verify the user owns this contact
|
|
850
|
+
const ownership = await db.contacts.verify(id, userId);
|
|
851
|
+
if (!ownership) {
|
|
852
|
+
return reply.status(403).send({ error: "Forbidden" });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return contact;
|
|
856
|
+
});
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### 9.3 Audit Logging for Portal Actions
|
|
860
|
+
|
|
861
|
+
```javascript
|
|
862
|
+
// Log all portal actions for compliance
|
|
863
|
+
async function logPortalAction(userId, action, resourceId, details) {
|
|
864
|
+
await db.auditLog.create({
|
|
865
|
+
userId,
|
|
866
|
+
action,
|
|
867
|
+
resourceId,
|
|
868
|
+
timestamp: new Date(),
|
|
869
|
+
ipAddress: details.ip,
|
|
870
|
+
userAgent: details.userAgent,
|
|
871
|
+
changes: details.changes,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
fastify.post("/api/contacts", async (request, reply) => {
|
|
876
|
+
const contact = await fastify.contacts.create(request.body);
|
|
877
|
+
|
|
878
|
+
await logPortalAction(request.user.id, "CREATE_CONTACT", contact.id, {
|
|
879
|
+
ip: request.ip,
|
|
880
|
+
userAgent: request.headers["user-agent"],
|
|
881
|
+
changes: request.body,
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
return contact;
|
|
885
|
+
});
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## 10. Compliance & Regulations
|
|
891
|
+
|
|
892
|
+
### 10.1 GDPR Compliance
|
|
893
|
+
|
|
894
|
+
**Data Processing Agreement (DPA):**
|
|
895
|
+
- Ensure you have a DPA with HubSpot
|
|
896
|
+
- Document all data processing activities
|
|
897
|
+
- Implement data retention policies
|
|
898
|
+
|
|
899
|
+
**User Rights:**
|
|
900
|
+
- **Right to Access:** Implement endpoint to export user data
|
|
901
|
+
- **Right to Erasure:** Implement contact deletion with audit trail
|
|
902
|
+
- **Right to Rectification:** Allow contact data updates
|
|
903
|
+
- **Right to Data Portability:** Export contact data in standard format
|
|
904
|
+
|
|
905
|
+
```javascript
|
|
906
|
+
// Implement GDPR data export endpoint
|
|
907
|
+
fastify.get("/api/user/data-export", async (request, reply) => {
|
|
908
|
+
const userId = request.user.id;
|
|
909
|
+
|
|
910
|
+
// Gather all user data
|
|
911
|
+
const contactData = await getContactData(userId);
|
|
912
|
+
const engagementData = await getEngagementData(userId);
|
|
913
|
+
const auditLog = await getAuditLog(userId);
|
|
914
|
+
|
|
915
|
+
// Return as JSON for portability
|
|
916
|
+
return {
|
|
917
|
+
exportDate: new Date().toISOString(),
|
|
918
|
+
dataSubject: userId,
|
|
919
|
+
contacts: contactData,
|
|
920
|
+
engagements: engagementData,
|
|
921
|
+
auditLog,
|
|
922
|
+
};
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// Implement right to erasure
|
|
926
|
+
fastify.delete("/api/user/data", async (request, reply) => {
|
|
927
|
+
const userId = request.user.id;
|
|
928
|
+
|
|
929
|
+
// Request confirmation
|
|
930
|
+
if (!request.body.confirmDeletion) {
|
|
931
|
+
return reply.status(400).send({
|
|
932
|
+
error: "Data deletion must be confirmed",
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Log deletion request
|
|
937
|
+
await logPortalAction(userId, "DATA_DELETION_REQUEST", userId, {
|
|
938
|
+
ip: request.ip,
|
|
939
|
+
reason: "GDPR Right to Erasure",
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// Delete all associated data
|
|
943
|
+
const contacts = await getPortalUserContacts(userId);
|
|
944
|
+
for (const contact of contacts) {
|
|
945
|
+
await fastify.contacts.delete(contact.id);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
await db.user.delete(userId);
|
|
949
|
+
|
|
950
|
+
return { success: true, message: "All data has been deleted" };
|
|
951
|
+
});
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### 10.2 CCPA Compliance (California)
|
|
955
|
+
|
|
956
|
+
Similar to GDPR but with specific timeline requirements:
|
|
957
|
+
- Must provide data access within 45 days
|
|
958
|
+
- Must allow "Do Not Sell My Personal Information" opt-out
|
|
959
|
+
- Must not discriminate against users exercising rights
|
|
960
|
+
|
|
961
|
+
### 10.3 Data Retention Policies
|
|
962
|
+
|
|
963
|
+
```javascript
|
|
964
|
+
// Implement automatic data retention/deletion
|
|
965
|
+
const DATA_RETENTION_DAYS = parseInt(process.env.DATA_RETENTION_DAYS || "365");
|
|
966
|
+
|
|
967
|
+
async function purgeOldContacts() {
|
|
968
|
+
const cutoffDate = new Date();
|
|
969
|
+
cutoffDate.setDate(cutoffDate.getDate() - DATA_RETENTION_DAYS);
|
|
970
|
+
|
|
971
|
+
const oldContacts = await db.contacts.find({
|
|
972
|
+
lastInteraction: { $lt: cutoffDate },
|
|
973
|
+
markedForDeletion: true,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
for (const contact of oldContacts) {
|
|
977
|
+
await fastify.contacts.delete(contact.hubspotId);
|
|
978
|
+
await db.contacts.delete(contact.id);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Run daily via cron job
|
|
983
|
+
schedule.scheduleJob("0 2 * * *", purgeOldContacts); // 2 AM daily
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
### 10.4 Compliance Monitoring
|
|
987
|
+
|
|
988
|
+
```javascript
|
|
989
|
+
// Monitor for compliance violations
|
|
990
|
+
async function monitorCompliance() {
|
|
991
|
+
// Check for suspicious activity
|
|
992
|
+
const suspiciousActivity = await db.auditLog.find({
|
|
993
|
+
action: "DELETE_CONTACT",
|
|
994
|
+
timestamp: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
if (suspiciousActivity.length > 100) {
|
|
998
|
+
// Alert security team
|
|
999
|
+
await sendSecurityAlert({
|
|
1000
|
+
type: "HIGH_DELETION_VOLUME",
|
|
1001
|
+
count: suspiciousActivity.length,
|
|
1002
|
+
period: "24 hours",
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Check for compliance violations
|
|
1007
|
+
const unencryptedTokens = await db.config.find({
|
|
1008
|
+
field: "apiKey",
|
|
1009
|
+
encrypted: false,
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
if (unencryptedTokens.length > 0) {
|
|
1013
|
+
await sendSecurityAlert({
|
|
1014
|
+
type: "UNENCRYPTED_SECRETS",
|
|
1015
|
+
count: unencryptedTokens.length,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
## Security Checklist
|
|
1024
|
+
|
|
1025
|
+
Before deploying to production:
|
|
1026
|
+
|
|
1027
|
+
- [ ] API key stored in environment variable, not hardcoded
|
|
1028
|
+
- [ ] Token rotation scheduled (every 90 days)
|
|
1029
|
+
- [ ] HTTPS/TLS enabled for all connections
|
|
1030
|
+
- [ ] Webhook signature validation implemented
|
|
1031
|
+
- [ ] Input validation and sanitization for all endpoints
|
|
1032
|
+
- [ ] Error messages don't expose internal details
|
|
1033
|
+
- [ ] Sensitive data never logged
|
|
1034
|
+
- [ ] Authentication and authorization implemented
|
|
1035
|
+
- [ ] Rate limiting configured
|
|
1036
|
+
- [ ] CORS configured with specific allowed origins
|
|
1037
|
+
- [ ] CSRF protection enabled
|
|
1038
|
+
- [ ] Audit logging implemented
|
|
1039
|
+
- [ ] Database backups configured
|
|
1040
|
+
- [ ] Security headers (CSP, HSTS, etc.) configured
|
|
1041
|
+
- [ ] Third-party dependencies up to date
|
|
1042
|
+
- [ ] Security testing completed
|
|
1043
|
+
- [ ] Incident response plan documented
|
|
1044
|
+
- [ ] Compliance requirements identified and implemented
|
|
1045
|
+
- [ ] Data retention policy defined
|
|
1046
|
+
- [ ] Privacy policy updated and compliant
|
|
1047
|
+
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
## Incident Response
|
|
1051
|
+
|
|
1052
|
+
If you suspect a security incident:
|
|
1053
|
+
|
|
1054
|
+
1. **Immediately revoke the compromised API token** in HubSpot admin
|
|
1055
|
+
2. **Generate a new API token** with same scopes
|
|
1056
|
+
3. **Update environment variables** with new token
|
|
1057
|
+
4. **Review audit logs** for unauthorized access
|
|
1058
|
+
5. **Notify affected users** if personal data was exposed
|
|
1059
|
+
6. **File required notifications** with regulators (GDPR/CCPA)
|
|
1060
|
+
7. **Conduct root cause analysis** to prevent future incidents
|
|
1061
|
+
8. **Document incident** for compliance records
|
|
1062
|
+
|
|
1063
|
+
---
|
|
1064
|
+
|
|
1065
|
+
## Additional Resources
|
|
1066
|
+
|
|
1067
|
+
- [HubSpot API Security](https://developers.hubspot.com/docs/api/overview)
|
|
1068
|
+
- [OWASP Top 10](https://owasp.org/Top10/)
|
|
1069
|
+
- [GDPR Compliance Guide](https://gdpr-info.eu/)
|
|
1070
|
+
- [CCPA Compliance Guide](https://www.ccpa.ca.gov/)
|
|
1071
|
+
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
|
|
1072
|
+
- [Fastify Security](https://www.fastify.io/docs/latest/Guides/Security/)
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
**Last Updated:** 2025-12-29
|
|
1077
|
+
**Maintainer:** Tim Mushen
|
|
1078
|
+
**License:** ISC
|