Claude-skill-registry apollo-data-handling
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/apollo-data-handling" ~/.claude/skills/majiayu000-claude-skill-registry-apollo-data-handling && rm -rf "$T"
manifest:
skills/data/apollo-data-handling/SKILL.mdsource content
Apollo Data Handling
Overview
Data management, compliance, and governance practices for Apollo.io contact data including GDPR, data retention, and secure handling.
Data Classification
| Data Type | Classification | Retention | Handling |
|---|---|---|---|
| Email addresses | PII | 2 years | Encrypted at rest |
| Phone numbers | PII | 2 years | Encrypted at rest |
| Names | PII | 2 years | Standard |
| Job titles | Business | 5 years | Standard |
| Company info | Business | 5 years | Standard |
| Engagement data | Analytics | 1 year | Aggregated |
GDPR Compliance
Right to Access (Subject Access Request)
// src/services/gdpr/access.service.ts import { Contact } from '../../models/contact.model'; import { Engagement } from '../../models/engagement.model'; interface SubjectAccessResponse { personalData: { contact: Partial<Contact>; engagements: Partial<Engagement>[]; }; processingPurposes: string[]; dataRetention: string; dataSources: string[]; } export async function handleSubjectAccessRequest( email: string ): Promise<SubjectAccessResponse> { // Find all data for this subject const contact = await prisma.contact.findFirst({ where: { email }, select: { id: true, email: true, name: true, firstName: true, lastName: true, title: true, phone: true, linkedinUrl: true, company: { select: { name: true, domain: true, }, }, createdAt: true, updatedAt: true, }, }); if (!contact) { return { personalData: { contact: {}, engagements: [] }, processingPurposes: [], dataRetention: 'No data found', dataSources: [], }; } const engagements = await prisma.engagement.findMany({ where: { contactId: contact.id }, select: { type: true, occurredAt: true, sequenceId: true, }, }); return { personalData: { contact, engagements, }, processingPurposes: [ 'B2B sales outreach', 'Lead qualification', 'Marketing communications', ], dataRetention: '2 years from last activity', dataSources: ['Apollo.io API', 'User-provided forms'], }; }
Right to Erasure (Right to be Forgotten)
// src/services/gdpr/erasure.service.ts interface ErasureResult { success: boolean; recordsDeleted: { contacts: number; engagements: number; sequences: number; }; apolloNotified: boolean; } export async function handleErasureRequest(email: string): Promise<ErasureResult> { const result: ErasureResult = { success: false, recordsDeleted: { contacts: 0, engagements: 0, sequences: 0 }, apolloNotified: false, }; try { // Start transaction await prisma.$transaction(async (tx) => { // Find contact const contact = await tx.contact.findFirst({ where: { email } }); if (!contact) { throw new Error('Contact not found'); } // Delete engagements const deletedEngagements = await tx.engagement.deleteMany({ where: { contactId: contact.id }, }); result.recordsDeleted.engagements = deletedEngagements.count; // Remove from sequences const deletedSequences = await tx.sequenceEnrollment.deleteMany({ where: { contactId: contact.id }, }); result.recordsDeleted.sequences = deletedSequences.count; // Delete contact await tx.contact.delete({ where: { id: contact.id } }); result.recordsDeleted.contacts = 1; // Notify Apollo (if they support it) try { await notifyApolloOfDeletion(email); result.apolloNotified = true; } catch (e) { console.warn('Could not notify Apollo of deletion', e); } }); result.success = true; // Log for audit await auditLog.create({ type: 'GDPR_ERASURE', subject: hashEmail(email), // Don't store email in audit log timestamp: new Date(), recordsAffected: result.recordsDeleted, }); return result; } catch (error) { console.error('Erasure request failed:', error); throw error; } } async function notifyApolloOfDeletion(email: string): Promise<void> { // Apollo doesn't have a deletion API, but we can: // 1. Remove from all sequences // 2. Mark as do-not-contact // 3. Open support ticket for full removal console.log(`Note: Contact Apollo support to fully delete ${email} from their system`); }
Consent Management
// src/services/consent/consent.service.ts import { z } from 'zod'; const ConsentSchema = z.object({ email: z.string().email(), purposes: z.array(z.enum([ 'sales_outreach', 'marketing_email', 'analytics', 'third_party_sharing', ])), timestamp: z.date(), source: z.string(), ipAddress: z.string().optional(), }); type Consent = z.infer<typeof ConsentSchema>; export async function recordConsent(consent: Consent): Promise<void> { await prisma.consent.create({ data: { email: consent.email, purposes: consent.purposes, grantedAt: consent.timestamp, source: consent.source, ipAddress: consent.ipAddress, }, }); } export async function checkConsent(email: string, purpose: string): Promise<boolean> { const consent = await prisma.consent.findFirst({ where: { email, purposes: { has: purpose }, revokedAt: null, }, orderBy: { grantedAt: 'desc' }, }); return !!consent; } export async function revokeConsent(email: string, purpose?: string): Promise<void> { if (purpose) { // Revoke specific purpose await prisma.consent.updateMany({ where: { email, purposes: { has: purpose } }, data: { revokedAt: new Date() }, }); } else { // Revoke all await prisma.consent.updateMany({ where: { email }, data: { revokedAt: new Date() }, }); } }
Data Retention
// src/jobs/data-retention.job.ts import { CronJob } from 'cron'; // Run daily at 2 AM const retentionJob = new CronJob('0 2 * * *', async () => { console.log('Starting data retention cleanup...'); const retentionPolicies = [ { table: 'contacts', field: 'lastActivityAt', maxAgeDays: 730 }, { table: 'engagements', field: 'occurredAt', maxAgeDays: 365 }, { table: 'auditLogs', field: 'createdAt', maxAgeDays: 2555 }, // 7 years ]; for (const policy of retentionPolicies) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - policy.maxAgeDays); const deleted = await prisma[policy.table].deleteMany({ where: { [policy.field]: { lt: cutoffDate }, }, }); console.log(`Deleted ${deleted.count} records from ${policy.table}`); } // Archive before deletion for compliance await archiveOldData(); }); async function archiveOldData(): Promise<void> { const archiveCutoff = new Date(); archiveCutoff.setDate(archiveCutoff.getDate() - 365); // Export to cold storage before deletion const oldContacts = await prisma.contact.findMany({ where: { lastActivityAt: { lt: archiveCutoff } }, }); if (oldContacts.length > 0) { await uploadToArchive('contacts', oldContacts); } }
Data Export
// src/services/export/export.service.ts import { stringify } from 'csv-stringify/sync'; import { createWriteStream } from 'fs'; import archiver from 'archiver'; interface ExportOptions { format: 'csv' | 'json'; includeEngagements: boolean; dateRange?: { start: Date; end: Date }; } export async function exportContactData( criteria: any, options: ExportOptions ): Promise<string> { const contacts = await prisma.contact.findMany({ where: { ...criteria, ...(options.dateRange && { createdAt: { gte: options.dateRange.start, lte: options.dateRange.end, }, }), }, include: options.includeEngagements ? { engagements: true } : undefined, }); const filename = `apollo-export-${Date.now()}`; if (options.format === 'csv') { const csv = stringify(contacts, { header: true, columns: ['id', 'email', 'name', 'title', 'company', 'createdAt'], }); await writeFile(`exports/${filename}.csv`, csv); return `${filename}.csv`; } else { await writeFile(`exports/${filename}.json`, JSON.stringify(contacts, null, 2)); return `${filename}.json`; } } export async function createSecureExport( contacts: any[], encryptionKey: string ): Promise<Buffer> { const data = JSON.stringify(contacts); const encrypted = await encrypt(data, encryptionKey); return encrypted; }
Data Encryption
// src/lib/encryption.ts import crypto from 'crypto'; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; export function encryptPII(plaintext: string, key: Buffer): string { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // Format: iv:authTag:ciphertext return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; } export function decryptPII(encrypted: string, key: Buffer): string { const [ivHex, authTagHex, ciphertext] = encrypted.split(':'); const iv = Buffer.from(ivHex, 'hex'); const authTag = Buffer.from(authTagHex, 'hex'); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // Column-level encryption for Prisma export const encryptedFields = { email: { encrypt: (value: string) => encryptPII(value, getEncryptionKey()), decrypt: (value: string) => decryptPII(value, getEncryptionKey()), }, phone: { encrypt: (value: string) => encryptPII(value, getEncryptionKey()), decrypt: (value: string) => decryptPII(value, getEncryptionKey()), }, };
Audit Logging
// src/services/audit/audit.service.ts interface AuditEntry { action: string; actor: string; resource: string; resourceId: string; changes?: Record<string, { old: any; new: any }>; metadata?: Record<string, any>; timestamp: Date; } export async function logDataAccess(entry: AuditEntry): Promise<void> { await prisma.auditLog.create({ data: { action: entry.action, actor: entry.actor, resource: entry.resource, resourceId: entry.resourceId, changes: entry.changes ? JSON.stringify(entry.changes) : null, metadata: entry.metadata ? JSON.stringify(entry.metadata) : null, occurredAt: entry.timestamp, }, }); } // Middleware to audit all data access export function auditMiddleware(req: any, res: any, next: any) { const originalSend = res.send; res.send = function(body: any) { // Log data access if (req.path.includes('/apollo/') && req.method === 'GET') { logDataAccess({ action: 'DATA_ACCESS', actor: req.user?.id || 'anonymous', resource: 'apollo_contact', resourceId: req.params.id || 'bulk', metadata: { path: req.path, query: req.query, responseSize: body?.length, }, timestamp: new Date(), }); } return originalSend.call(this, body); }; next(); }
Output
- GDPR compliance (access, erasure, consent)
- Data retention policies
- Secure data export
- Column-level encryption
- Comprehensive audit logging
Error Handling
| Issue | Resolution |
|---|---|
| Export too large | Implement streaming |
| Encryption key lost | Use key management service |
| Audit log gaps | Implement retry queue |
| Consent conflicts | Use latest consent record |
Resources
Next Steps
Proceed to
apollo-enterprise-rbac for access control.