@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.
- package/package.json +1 -1
- package/CHANGELOG.md +0 -295
- 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.
|
|
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
|