@xenterprises/fastify-xhubspot 1.1.0 → 1.1.1

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/CHANGELOG.md +0 -295
  3. package/SECURITY.md +0 -1078
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xhubspot",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.1.1",
5
5
  "description": "Fastify plugin for HubSpot CRM integration with contact management, engagement tracking, and custom objects support. Ideal for third-party portals managing contacts, companies, deals, and engagement notes.",
6
6
  "main": "src/xHubspot.js",
7
7
  "exports": {
package/CHANGELOG.md DELETED
@@ -1,295 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ---
9
-
10
- ## [1.0.0] - 2025-12-29
11
-
12
- ### ✨ Initial Release
13
-
14
- #### Added
15
-
16
- **Core Plugin Features:**
17
- - Complete Fastify v5 plugin with fastify-plugin integration
18
- - Lazy initialization of HubSpot client
19
- - API key validation on startup
20
- - Comprehensive error handling with correlation IDs
21
- - Request logging capability with sensitive data masking
22
- - Module decoration pattern for Fastify instance
23
-
24
- **Contact Management Service:**
25
- - `create(contactData)` - Create individual contacts
26
- - `getById(contactId, properties)` - Retrieve contact by HubSpot ID
27
- - `getByEmail(email, properties)` - Retrieve contact by email (idempotent key)
28
- - `update(contactId, properties)` - Update contact properties
29
- - `delete(contactId)` - Archive/delete contacts
30
- - `list(options)` - Paginated contact listing with filtering
31
- - `search(property, value, options)` - Search contacts by property
32
- - `batchCreate(contacts)` - Bulk create up to 100 contacts per call
33
- - `batchUpdate(contacts)` - Bulk update up to 100 contacts per call
34
- - `getAssociations(contactId, associationType)` - Retrieve associated objects
35
- - `associate(contactId, objectId, associationType)` - Create associations
36
-
37
- **Company Management Service:**
38
- - `create(companyData)` - Create companies
39
- - `getById(companyId, properties)` - Retrieve company by ID
40
- - `getByDomain(domain, properties)` - Retrieve company by domain
41
- - `update(companyId, properties)` - Update company properties
42
- - `delete(companyId)` - Archive/delete companies
43
- - `list(options)` - Paginated company listing
44
- - `search(property, value)` - Search companies by property
45
- - `batchCreate(companies)` - Bulk create up to 100 companies
46
- - `getAssociations(companyId)` - Retrieve associated contacts/deals
47
-
48
- **Deal Management Service:**
49
- - `create(dealData)` - Create deals in HubSpot sales pipeline
50
- - `getById(dealId, properties)` - Retrieve deal by ID
51
- - `update(dealId, properties)` - Update deal properties
52
- - `delete(dealId)` - Archive/delete deals
53
- - `list(options)` - Paginated deal listing with pipeline filtering
54
- - `getAssociations(dealId)` - Retrieve associated contacts/companies
55
-
56
- **Engagement/Activity Service:**
57
- - `createNote(contactId, body, ownerId)` - Create engagement notes
58
- - `createTask(contactId, title, options)` - Create tasks with priority and due dates
59
- - `createCall(contactId, callResult, options)` - Log call engagements
60
- - `createEmail(contactId, subject, body, options)` - Log email engagements
61
- - `getEngagements(options)` - Retrieve contact engagement history
62
- - `getNotes(contactId, limit)` - Retrieve notes for contact
63
- - `getTasks(contactId, limit)` - Retrieve tasks for contact
64
-
65
- **Custom Objects Service:**
66
- - `create(objectType, properties)` - Create custom objects
67
- - `getById(objectType, objectId, properties)` - Retrieve custom objects
68
- - `update(objectType, objectId, properties)` - Update custom objects
69
- - `delete(objectType, objectId)` - Delete custom objects
70
- - `list(options)` - List custom objects with pagination
71
- - `getAssociations(objectType, objectId)` - Retrieve custom object associations
72
-
73
- **Configuration & Environment:**
74
- - Comprehensive `.env.example` with 100+ configuration options
75
- - Environment variable support for:
76
- - HubSpot API configuration
77
- - Contact/company/deal/custom object properties
78
- - Batch operation sizes
79
- - Rate limiting
80
- - Data validation rules
81
- - Webhook configuration
82
- - Logging levels
83
- - Feature flags for selective enablement
84
-
85
- **Documentation:**
86
- - TypeScript definitions (index.d.ts) with full type safety
87
- - Comprehensive SECURITY.md with 10 security sections:
88
- - API key management
89
- - Authentication & authorization
90
- - Data protection & privacy
91
- - Webhook security
92
- - Request validation & sanitization
93
- - Error handling & logging
94
- - Rate limiting & DOS protection
95
- - Network security
96
- - Third-party portal integration
97
- - Compliance & regulations (GDPR, CCPA)
98
- - CHANGELOG.md tracking all versions
99
- - LICENSE (ISC)
100
-
101
- **Testing:**
102
- - Basic test structure in place
103
- - Plugin registration validation
104
- - Service availability checks
105
-
106
- **Docker Support:**
107
- - Dockerfile with multi-stage build
108
- - docker-compose.yml configuration
109
- - .dockerignore file
110
-
111
- ### 📋 Known Limitations
112
-
113
- - Single browser instance for Puppeteer (in future xPDF integration)
114
- - No connection pooling for HubSpot API (uses single client)
115
- - Webhook processing is synchronous (async processing recommended for production)
116
- - No built-in caching (Redis integration recommended for high-volume reads)
117
-
118
- ### 🔒 Security Features
119
-
120
- - API token validation on startup
121
- - HTTPS enforcement in production
122
- - Webhook signature validation support
123
- - Email and phone number validation
124
- - Blocked email domain configuration
125
- - Sensitive data logging control
126
- - Request logging with data masking
127
- - Rate limiting configuration
128
- - GDPR/CCPA compliance guidance
129
-
130
- ### 🚀 Performance Considerations
131
-
132
- - Lazy client initialization (creates HubSpot client on first use)
133
- - Batch operations support (up to 100 items per call)
134
- - Configurable rate limiting (10-500 req/sec based on tier)
135
- - Retry logic with exponential backoff
136
- - Connection reuse for all operations
137
-
138
- ### 📚 Dependencies
139
-
140
- #### Runtime
141
- - `fastify-plugin`: ^5.0.0
142
- - `@hubspot/api-client`: ^15.0.0 (or latest)
143
-
144
- #### Development
145
- - `fastify`: ^5.1.0
146
- - `node`: ^20.0.0
147
-
148
- ---
149
-
150
- ## [Unreleased] - Future Enhancements
151
-
152
- ### Planned for v1.1.0
153
-
154
- **Features:**
155
- - [ ] Contact deduplication API
156
- - [ ] Contact merge functionality
157
- - [ ] Advanced contact search with filters
158
- - [ ] Deal pipeline stages API
159
- - [ ] Deal stage automation
160
- - [ ] Engagement batch operations
161
- - [ ] Email template support
162
- - [ ] SMS integration (via Twilio bridge)
163
- - [ ] Workflow automation integration
164
- - [ ] Custom property definitions API
165
-
166
- **Performance:**
167
- - [ ] Connection pooling
168
- - [ ] Request caching layer
169
- - [ ] Batch operation queuing
170
- - [ ] Response compression
171
-
172
- **Developer Experience:**
173
- - [ ] GraphQL support
174
- - [ ] Webhook sandbox environment
175
- - [ ] Mock API for testing
176
- - [ ] CLI tools for bulk operations
177
- - [ ] Database schema sync utilities
178
-
179
- ### Planned for v1.2.0
180
-
181
- **Features:**
182
- - [ ] List/segment management
183
- - [ ] Email campaign integration
184
- - [ ] Chatbot/conversation API
185
- - [ ] Landing page management
186
- - [ ] Form submission handling
187
- - [ ] Invoice generation
188
- - [ ] Advanced reporting
189
- - [ ] Custom field type support
190
-
191
- **Compliance:**
192
- - [ ] HIPAA compliance guidelines
193
- - [ ] SOC 2 compliance documentation
194
- - [ ] Data residency options
195
- - [ ] Encryption at rest
196
-
197
- **Infrastructure:**
198
- - [ ] Kubernetes manifests
199
- - [ ] Helm charts
200
- - [ ] Terraform modules
201
- - [ ] Load balancer configuration
202
-
203
- ### Planned for v2.0.0 (Major Release)
204
-
205
- **Breaking Changes:**
206
- - Migration to ES modules only (drop CommonJS)
207
- - Required Node.js 22+
208
- - Fastify v6+ support
209
-
210
- **New Features:**
211
- - [ ] GraphQL API server
212
- - [ ] Real-time webhooks with WebSocket
213
- - [ ] AI-powered contact recommendations
214
- - [ ] Advanced analytics dashboard
215
- - [ ] Multi-tenant support
216
- - [ ] Custom database backends
217
-
218
- **Performance:**
219
- - [ ] Edge function support (Vercel, Cloudflare)
220
- - [ ] Serverless function optimization
221
- - [ ] Micro-service architecture support
222
-
223
- ---
224
-
225
- ## Version History
226
-
227
- ### v1.0.0 - 2025-12-29
228
- - ✅ Initial production release
229
- - ✅ All core features implemented
230
- - ✅ Full documentation and type definitions
231
- - ✅ Comprehensive security guidelines
232
- - ✅ Docker support
233
-
234
- ---
235
-
236
- ## Migration Guides
237
-
238
- ### Upgrading from Beta to v1.0.0
239
-
240
- No breaking changes - this is the first stable release.
241
-
242
- ### Future v1.0.0 → v1.1.0 Upgrade
243
-
244
- Expected to be non-breaking. No API changes expected.
245
-
246
- ### Future v1.x → v2.0.0 Upgrade
247
-
248
- Will include breaking changes:
249
- - CommonJS removed (ES modules only)
250
- - Node.js 22+ required
251
- - Fastify v6+ required
252
- - New API design for some services
253
-
254
- ---
255
-
256
- ## Release Schedule
257
-
258
- - **v1.0.0**: 2025-12-29 (Initial release)
259
- - **v1.1.0**: Estimated Q1 2026
260
- - **v1.2.0**: Estimated Q2 2026
261
- - **v2.0.0**: Estimated Q4 2026 (major rewrite)
262
-
263
- ---
264
-
265
- ## Support & Security Updates
266
-
267
- | Version | Release Date | End of Life | Security Updates |
268
- |---------|------------|----------|-----------------|
269
- | 1.0.x | 2025-12-29 | 2027-12-29 | Until EOL |
270
- | 1.1.x | 2026-Q1 | 2027-Q1 | Until EOL |
271
- | 2.0.x | 2026-Q4 | 2028-Q4 | Until EOL |
272
-
273
- ---
274
-
275
- ## Contributing
276
-
277
- Please follow these guidelines when submitting changes:
278
-
279
- 1. Reference an issue number in your PR
280
- 2. Include a test for new features
281
- 3. Update CHANGELOG.md in your PR
282
- 4. Follow [Keep a Changelog](https://keepachangelog.com/) format
283
- 5. Use semantic versioning for version bumps
284
-
285
- ---
286
-
287
- ## Security Announcements
288
-
289
- None currently. All security issues should be reported privately to the maintainer.
290
-
291
- ---
292
-
293
- **Last Updated:** 2025-12-29
294
- **Maintainer:** Tim Mushen
295
- **License:** ISC
package/SECURITY.md DELETED
@@ -1,1078 +0,0 @@
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