@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/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