[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6F6C7PWXK3R39B6P3JDXYG4":3,"skill-same:01M6F6C7PWXK3R39B6P3JDXYG4":82},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":50,"linkOnly":51,"markdownContent":52,"markdownBody":53,"frontMatter":54,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":62,"repositoryHtmlUrl":63,"repositoryHomepageUrl":6,"ownerHtmlUrl":64,"ownerBlogUrl":65,"ownerDescription":66,"updatedAt":67,"install":68,"resources":81},"01M6F6C7PWXK3R39B6P3JDXYG4","documenso-data-handling",null,"Handle document data, signatures, and PII in Documenso integrations.\nUse when managing document lifecycle, handling signed PDFs,\nor implementing data retention policies.\nTrigger with phrases like \"documenso data\", \"signed document\",\n\"document retention\", \"documenso PII\", \"download signed pdf\".","Dicklesworthstone\u002Fpi_agent_rust","Dicklesworthstone","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F35050222?v=4","01M6CG26AR942CSVMN1M5E3DSE","Jeff Emanuel","01M6CG26EPP71PSHW1VDS71121",1992,"MIT","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Fplugins-community\u002Fplugins\u002Fsaas-packs\u002Fdocumenso-pack\u002Fskills\u002Fdocumenso-data-handling\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000002","文档处理","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"data","documenso","documenso data handling","when","PDF","implementing","with","retention","trigger","io","signatures","document","use","edit","write","read","integrations","download","document lifecycle","lifecycle","pii","2026-11-10 10:12:30",false,"---\nname: documenso-data-handling\ndescription: |\n  Handle document data, signatures, and PII in Documenso integrations.\n  Use when managing document lifecycle, handling signed PDFs,\n  or implementing data retention policies.\n  Trigger with phrases like \"documenso data\", \"signed document\",\n  \"document retention\", \"documenso PII\", \"download signed pdf\".\nallowed-tools: Read, Write, Edit\nversion: 1.0.0\nlicense: MIT\nauthor: Jeremy Longshore \u003Cjeremy@intentsolutions.io>\n---\n\n# Documenso Data Handling\n\n## Overview\nBest practices for handling documents, signatures, and personally identifiable information (PII) in Documenso integrations.\n\n## Prerequisites\n- Understanding of data protection regulations (GDPR, CCPA)\n- Secure storage infrastructure\n- Encryption capabilities\n\n## Document Lifecycle\n\n```\n┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n│   DRAFT     │────▶│   PENDING   │────▶│  COMPLETED  │\n│             │     │  (Signing)  │     │             │\n└─────────────┘     └──────┬──────┘     └──────┬──────┘\n                           │                    │\n                           ▼                    │\n                    ┌─────────────┐             │\n                    │  REJECTED\u002F  │             │\n                    │  CANCELLED  │             │\n                    └─────────────┘             │\n                                               ▼\n                                        ┌─────────────┐\n                                        │  ARCHIVED   │\n                                        │  (Storage)  │\n                                        └─────────────┘\n```\n\n## Downloading Signed Documents\n\n### Step 1: Download Completed Document\n\n```typescript\nimport { getDocumensoClient } from \".\u002Fdocumenso\u002Fclient\";\nimport fs from \"fs\u002Fpromises\";\n\nasync function downloadSignedDocument(\n  documentId: string,\n  outputPath: string\n): Promise\u003Cvoid> {\n  const client = getDocumensoClient();\n\n  \u002F\u002F Verify document is completed\n  const doc = await client.documents.getV0({ documentId });\n\n  if (doc.status !== \"COMPLETED\") {\n    throw new Error(`Document not completed. Status: ${doc.status}`);\n  }\n\n  \u002F\u002F Download the signed PDF\n  const pdfData = await client.documents.downloadV0({ documentId });\n\n  \u002F\u002F Save to file\n  await fs.writeFile(outputPath, Buffer.from(pdfData as ArrayBuffer));\n\n  console.log(`Signed document saved to: ${outputPath}`);\n}\n```\n\n### Step 2: Secure Storage\n\n```typescript\nimport { S3Client, PutObjectCommand } from \"@aws-sdk\u002Fclient-s3\";\nimport crypto from \"crypto\";\n\nconst s3 = new S3Client({ region: process.env.AWS_REGION });\n\ninterface StorageResult {\n  key: string;\n  bucket: string;\n  checksum: string;\n}\n\nasync function storeSignedDocument(\n  documentId: string,\n  pdfData: Buffer,\n  metadata: Record\u003Cstring, string>\n): Promise\u003CStorageResult> {\n  \u002F\u002F Generate unique key with date partitioning\n  const date = new Date();\n  const key = `signed-documents\u002F${date.getFullYear()}\u002F${\n    String(date.getMonth() + 1).padStart(2, \"0\")\n  }\u002F${documentId}.pdf`;\n\n  \u002F\u002F Calculate checksum for integrity verification\n  const checksum = crypto.createHash(\"sha256\").update(pdfData).digest(\"hex\");\n\n  \u002F\u002F Upload with server-side encryption\n  await s3.send(\n    new PutObjectCommand({\n      Bucket: process.env.DOCUMENTS_BUCKET!,\n      Key: key,\n      Body: pdfData,\n      ContentType: \"application\u002Fpdf\",\n      ServerSideEncryption: \"aws:kms\",\n      SSEKMSKeyId: process.env.KMS_KEY_ID,\n      Metadata: {\n        ...metadata,\n        checksum,\n        uploadedAt: new Date().toISOString(),\n      },\n    })\n  );\n\n  return {\n    key,\n    bucket: process.env.DOCUMENTS_BUCKET!,\n    checksum,\n  };\n}\n```\n\n## PII Handling\n\n### Step 3: Recipient Data Management\n\n```typescript\ninterface RecipientPII {\n  email: string;\n  name: string;\n  documentIds: string[];\n}\n\n\u002F\u002F Store PII reference mapping (not the actual PII)\ninterface PIIReference {\n  recipientHash: string;  \u002F\u002F Hashed identifier\n  documentIds: string[];\n  createdAt: Date;\n  lastAccessedAt: Date;\n}\n\nfunction hashRecipientEmail(email: string): string {\n  const salt = process.env.PII_SALT!;\n  return crypto\n    .createHmac(\"sha256\", salt)\n    .update(email.toLowerCase().trim())\n    .digest(\"hex\");\n}\n\n\u002F\u002F Track document associations without storing raw PII\nasync function trackRecipientDocument(\n  email: string,\n  documentId: string\n): Promise\u003Cvoid> {\n  const recipientHash = hashRecipientEmail(email);\n\n  await db.piiReferences.upsert({\n    where: { recipientHash },\n    create: {\n      recipientHash,\n      documentIds: [documentId],\n      createdAt: new Date(),\n      lastAccessedAt: new Date(),\n    },\n    update: {\n      documentIds: { push: documentId },\n      lastAccessedAt: new Date(),\n    },\n  });\n}\n```\n\n### Step 4: Data Minimization\n\n```typescript\n\u002F\u002F Only request necessary recipient information\nasync function addRecipientMinimal(\n  documentId: string,\n  email: string,\n  name: string\n): Promise\u003Cstring> {\n  const client = getDocumensoClient();\n\n  \u002F\u002F Don't store additional PII we don't need\n  const recipient = await client.documentsRecipients.createV0({\n    documentId,\n    email,\n    name,\n    role: \"SIGNER\",\n    \u002F\u002F Don't add phone, address, or other unnecessary PII\n  });\n\n  return recipient.recipientId!;\n}\n\n\u002F\u002F Sanitize document data before logging\nfunction sanitizeForLogging(doc: any): any {\n  return {\n    id: doc.id,\n    title: doc.title,\n    status: doc.status,\n    recipientCount: doc.recipients?.length ?? 0,\n    \u002F\u002F Explicitly exclude: recipients[].email, recipients[].name\n  };\n}\n```\n\n## Data Retention\n\n### Step 5: Retention Policy Implementation\n\n```typescript\ninterface RetentionPolicy {\n  completedDocuments: number;  \u002F\u002F Days to retain completed docs\n  draftDocuments: number;      \u002F\u002F Days to retain drafts\n  cancelledDocuments: number;  \u002F\u002F Days to retain cancelled\n}\n\nconst RETENTION_POLICY: RetentionPolicy = {\n  completedDocuments: 2555,  \u002F\u002F 7 years for legal compliance\n  draftDocuments: 30,        \u002F\u002F 30 days for abandoned drafts\n  cancelledDocuments: 90,    \u002F\u002F 90 days for cancelled\n};\n\nasync function enforceRetentionPolicy(): Promise\u003CRetentionReport> {\n  const report = {\n    draftsDeleted: 0,\n    cancelledDeleted: 0,\n    archivedCompleted: 0,\n  };\n\n  const client = getDocumensoClient();\n  const now = new Date();\n\n  \u002F\u002F Find old drafts\n  const drafts = await findDocumentsByStatus(\"DRAFT\");\n  for (const draft of drafts) {\n    const age = daysSince(new Date(draft.createdAt!));\n    if (age > RETENTION_POLICY.draftDocuments) {\n      await client.documents.deleteV0({ documentId: draft.id! });\n      report.draftsDeleted++;\n    }\n  }\n\n  \u002F\u002F Find old cancelled documents\n  const cancelled = await findDocumentsByStatus(\"CANCELLED\");\n  for (const doc of cancelled) {\n    const age = daysSince(new Date(doc.updatedAt!));\n    if (age > RETENTION_POLICY.cancelledDocuments) {\n      await archiveAndDelete(doc.id!);\n      report.cancelledDeleted++;\n    }\n  }\n\n  return report;\n}\n\nfunction daysSince(date: Date): number {\n  const now = new Date();\n  return Math.floor((now.getTime() - date.getTime()) \u002F (1000 * 60 * 60 * 24));\n}\n```\n\n### Step 6: Archive Before Delete\n\n```typescript\ninterface ArchiveRecord {\n  documentId: string;\n  title: string;\n  status: string;\n  completedAt?: string;\n  recipientEmails: string[];  \u002F\u002F Hashed\n  storageKey: string;\n  archivedAt: string;\n}\n\nasync function archiveAndDelete(documentId: string): Promise\u003Cvoid> {\n  const client = getDocumensoClient();\n\n  \u002F\u002F Get document details\n  const doc = await client.documents.getV0({ documentId });\n\n  \u002F\u002F Download PDF if completed\n  let storageKey = \"\";\n  if (doc.status === \"COMPLETED\") {\n    const pdfData = await client.documents.downloadV0({ documentId });\n    const result = await storeSignedDocument(\n      documentId,\n      Buffer.from(pdfData as ArrayBuffer),\n      { title: doc.title!, status: doc.status }\n    );\n    storageKey = result.key;\n  }\n\n  \u002F\u002F Create archive record\n  const archiveRecord: ArchiveRecord = {\n    documentId,\n    title: doc.title!,\n    status: doc.status!,\n    completedAt: doc.completedAt,\n    recipientEmails: doc.recipients?.map((r) =>\n      hashRecipientEmail(r.email!)\n    ) ?? [],\n    storageKey,\n    archivedAt: new Date().toISOString(),\n  };\n\n  \u002F\u002F Save to archive database\n  await db.documentArchive.create({ data: archiveRecord });\n\n  \u002F\u002F Delete from Documenso\n  await client.documents.deleteV0({ documentId });\n\n  console.log(`Archived and deleted document: ${documentId}`);\n}\n```\n\n## GDPR Compliance\n\n### Step 7: Data Subject Access Request (DSAR)\n\n```typescript\ninterface DSARResponse {\n  recipientHash: string;\n  documents: Array\u003C{\n    documentId: string;\n    title: string;\n    status: string;\n    signedAt?: string;\n  }>;\n  exportedAt: string;\n}\n\nasync function handleDSAR(email: string): Promise\u003CDSARResponse> {\n  const recipientHash = hashRecipientEmail(email);\n  const client = getDocumensoClient();\n\n  \u002F\u002F Find all documents associated with this email\n  const documents: DSARResponse[\"documents\"] = [];\n\n  \u002F\u002F Search through documents (pagination required for large datasets)\n  let page = 1;\n  let hasMore = true;\n\n  while (hasMore) {\n    const result = await client.documents.findV0({ page, perPage: 100 });\n\n    for (const doc of result.documents ?? []) {\n      const hasRecipient = doc.recipients?.some(\n        (r) => r.email?.toLowerCase() === email.toLowerCase()\n      );\n\n      if (hasRecipient) {\n        const recipient = doc.recipients?.find(\n          (r) => r.email?.toLowerCase() === email.toLowerCase()\n        );\n\n        documents.push({\n          documentId: doc.id!,\n          title: doc.title!,\n          status: doc.status!,\n          signedAt: recipient?.signedAt,\n        });\n      }\n    }\n\n    hasMore = (result.documents?.length ?? 0) === 100;\n    page++;\n  }\n\n  return {\n    recipientHash,\n    documents,\n    exportedAt: new Date().toISOString(),\n  };\n}\n```\n\n### Step 8: Right to Erasure\n\n```typescript\nasync function handleErasureRequest(email: string): Promise\u003CErasureReport> {\n  const report = {\n    documentsAffected: 0,\n    cannotDelete: [] as string[],\n    deleted: [] as string[],\n  };\n\n  const dsar = await handleDSAR(email);\n\n  for (const doc of dsar.documents) {\n    \u002F\u002F Cannot delete completed documents due to legal requirements\n    if (doc.status === \"COMPLETED\") {\n      report.cannotDelete.push(doc.documentId);\n      console.log(\n        `Cannot delete ${doc.documentId}: Legal retention required`\n      );\n      continue;\n    }\n\n    \u002F\u002F Can delete drafts and cancelled\n    if (doc.status === \"DRAFT\" || doc.status === \"CANCELLED\") {\n      const client = getDocumensoClient();\n      await client.documents.deleteV0({ documentId: doc.documentId });\n      report.deleted.push(doc.documentId);\n    }\n\n    report.documentsAffected++;\n  }\n\n  return report;\n}\n```\n\n## Encryption at Rest\n\n```typescript\nimport crypto from \"crypto\";\n\nconst ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, \"hex\");\n\nfunction encryptPII(plaintext: string): string {\n  const iv = crypto.randomBytes(16);\n  const cipher = crypto.createCipheriv(\"aes-256-gcm\", ENCRYPTION_KEY, iv);\n\n  let encrypted = cipher.update(plaintext, \"utf8\", \"hex\");\n  encrypted += cipher.final(\"hex\");\n\n  const authTag = cipher.getAuthTag().toString(\"hex\");\n\n  return `${iv.toString(\"hex\")}:${authTag}:${encrypted}`;\n}\n\nfunction decryptPII(ciphertext: string): string {\n  const [ivHex, authTagHex, encrypted] = ciphertext.split(\":\");\n\n  const iv = Buffer.from(ivHex, \"hex\");\n  const authTag = Buffer.from(authTagHex, \"hex\");\n\n  const decipher = crypto.createDecipheriv(\"aes-256-gcm\", ENCRYPTION_KEY, iv);\n  decipher.setAuthTag(authTag);\n\n  let decrypted = decipher.update(encrypted, \"hex\", \"utf8\");\n  decrypted += decipher.final(\"utf8\");\n\n  return decrypted;\n}\n```\n\n## Output\n- Signed documents securely stored\n- PII properly protected\n- Retention policies enforced\n- GDPR compliance implemented\n\n## Error Handling\n| Data Issue | Cause | Solution |\n|------------|-------|----------|\n| Download failed | Document not complete | Check status first |\n| Storage failed | Permissions | Check bucket policy |\n| Decryption failed | Wrong key | Verify encryption key |\n| DSAR incomplete | Pagination | Handle all pages |\n\n## Resources\n- [GDPR Requirements](https:\u002F\u002Fgdpr.eu\u002F)\n- [Document Retention Laws](https:\u002F\u002Fwww.nist.gov\u002Fpublications)\n- [AWS S3 Encryption](https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonS3\u002Flatest\u002Fuserguide\u002Fserv-side-encryption.html)\n\n## Next Steps\nFor enterprise RBAC, see `documenso-enterprise-rbac`.\n","# Documenso Data Handling\n\n## Overview\nBest practices for handling documents, signatures, and personally identifiable information (PII) in Documenso integrations.\n\n## Prerequisites\n- Understanding of data protection regulations (GDPR, CCPA)\n- Secure storage infrastructure\n- Encryption capabilities\n\n## Document Lifecycle\n\n```\n┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n│   DRAFT     │────▶│   PENDING   │────▶│  COMPLETED  │\n│             │     │  (Signing)  │     │             │\n└─────────────┘     └──────┬──────┘     └──────┬──────┘\n                           │                    │\n                           ▼                    │\n                    ┌─────────────┐             │\n                    │  REJECTED\u002F  │             │\n                    │  CANCELLED  │             │\n                    └─────────────┘             │\n                                               ▼\n                                        ┌─────────────┐\n                                        │  ARCHIVED   │\n                                        │  (Storage)  │\n                                        └─────────────┘\n```\n\n## Downloading Signed Documents\n\n### Step 1: Download Completed Document\n\n```typescript\nimport { getDocumensoClient } from \".\u002Fdocumenso\u002Fclient\";\nimport fs from \"fs\u002Fpromises\";\n\nasync function downloadSignedDocument(\n  documentId: string,\n  outputPath: string\n): Promise\u003Cvoid> {\n  const client = getDocumensoClient();\n\n  \u002F\u002F Verify document is completed\n  const doc = await client.documents.getV0({ documentId });\n\n  if (doc.status !== \"COMPLETED\") {\n    throw new Error(`Document not completed. Status: ${doc.status}`);\n  }\n\n  \u002F\u002F Download the signed PDF\n  const pdfData = await client.documents.downloadV0({ documentId });\n\n  \u002F\u002F Save to file\n  await fs.writeFile(outputPath, Buffer.from(pdfData as ArrayBuffer));\n\n  console.log(`Signed document saved to: ${outputPath}`);\n}\n```\n\n### Step 2: Secure Storage\n\n```typescript\nimport { S3Client, PutObjectCommand } from \"@aws-sdk\u002Fclient-s3\";\nimport crypto from \"crypto\";\n\nconst s3 = new S3Client({ region: process.env.AWS_REGION });\n\ninterface StorageResult {\n  key: string;\n  bucket: string;\n  checksum: string;\n}\n\nasync function storeSignedDocument(\n  documentId: string,\n  pdfData: Buffer,\n  metadata: Record\u003Cstring, string>\n): Promise\u003CStorageResult> {\n  \u002F\u002F Generate unique key with date partitioning\n  const date = new Date();\n  const key = `signed-documents\u002F${date.getFullYear()}\u002F${\n    String(date.getMonth() + 1).padStart(2, \"0\")\n  }\u002F${documentId}.pdf`;\n\n  \u002F\u002F Calculate checksum for integrity verification\n  const checksum = crypto.createHash(\"sha256\").update(pdfData).digest(\"hex\");\n\n  \u002F\u002F Upload with server-side encryption\n  await s3.send(\n    new PutObjectCommand({\n      Bucket: process.env.DOCUMENTS_BUCKET!,\n      Key: key,\n      Body: pdfData,\n      ContentType: \"application\u002Fpdf\",\n      ServerSideEncryption: \"aws:kms\",\n      SSEKMSKeyId: process.env.KMS_KEY_ID,\n      Metadata: {\n        ...metadata,\n        checksum,\n        uploadedAt: new Date().toISOString(),\n      },\n    })\n  );\n\n  return {\n    key,\n    bucket: process.env.DOCUMENTS_BUCKET!,\n    checksum,\n  };\n}\n```\n\n## PII Handling\n\n### Step 3: Recipient Data Management\n\n```typescript\ninterface RecipientPII {\n  email: string;\n  name: string;\n  documentIds: string[];\n}\n\n\u002F\u002F Store PII reference mapping (not the actual PII)\ninterface PIIReference {\n  recipientHash: string;  \u002F\u002F Hashed identifier\n  documentIds: string[];\n  createdAt: Date;\n  lastAccessedAt: Date;\n}\n\nfunction hashRecipientEmail(email: string): string {\n  const salt = process.env.PII_SALT!;\n  return crypto\n    .createHmac(\"sha256\", salt)\n    .update(email.toLowerCase().trim())\n    .digest(\"hex\");\n}\n\n\u002F\u002F Track document associations without storing raw PII\nasync function trackRecipientDocument(\n  email: string,\n  documentId: string\n): Promise\u003Cvoid> {\n  const recipientHash = hashRecipientEmail(email);\n\n  await db.piiReferences.upsert({\n    where: { recipientHash },\n    create: {\n      recipientHash,\n      documentIds: [documentId],\n      createdAt: new Date(),\n      lastAccessedAt: new Date(),\n    },\n    update: {\n      documentIds: { push: documentId },\n      lastAccessedAt: new Date(),\n    },\n  });\n}\n```\n\n### Step 4: Data Minimization\n\n```typescript\n\u002F\u002F Only request necessary recipient information\nasync function addRecipientMinimal(\n  documentId: string,\n  email: string,\n  name: string\n): Promise\u003Cstring> {\n  const client = getDocumensoClient();\n\n  \u002F\u002F Don't store additional PII we don't need\n  const recipient = await client.documentsRecipients.createV0({\n    documentId,\n    email,\n    name,\n    role: \"SIGNER\",\n    \u002F\u002F Don't add phone, address, or other unnecessary PII\n  });\n\n  return recipient.recipientId!;\n}\n\n\u002F\u002F Sanitize document data before logging\nfunction sanitizeForLogging(doc: any): any {\n  return {\n    id: doc.id,\n    title: doc.title,\n    status: doc.status,\n    recipientCount: doc.recipients?.length ?? 0,\n    \u002F\u002F Explicitly exclude: recipients[].email, recipients[].name\n  };\n}\n```\n\n## Data Retention\n\n### Step 5: Retention Policy Implementation\n\n```typescript\ninterface RetentionPolicy {\n  completedDocuments: number;  \u002F\u002F Days to retain completed docs\n  draftDocuments: number;      \u002F\u002F Days to retain drafts\n  cancelledDocuments: number;  \u002F\u002F Days to retain cancelled\n}\n\nconst RETENTION_POLICY: RetentionPolicy = {\n  completedDocuments: 2555,  \u002F\u002F 7 years for legal compliance\n  draftDocuments: 30,        \u002F\u002F 30 days for abandoned drafts\n  cancelledDocuments: 90,    \u002F\u002F 90 days for cancelled\n};\n\nasync function enforceRetentionPolicy(): Promise\u003CRetentionReport> {\n  const report = {\n    draftsDeleted: 0,\n    cancelledDeleted: 0,\n    archivedCompleted: 0,\n  };\n\n  const client = getDocumensoClient();\n  const now = new Date();\n\n  \u002F\u002F Find old drafts\n  const drafts = await findDocumentsByStatus(\"DRAFT\");\n  for (const draft of drafts) {\n    const age = daysSince(new Date(draft.createdAt!));\n    if (age > RETENTION_POLICY.draftDocuments) {\n      await client.documents.deleteV0({ documentId: draft.id! });\n      report.draftsDeleted++;\n    }\n  }\n\n  \u002F\u002F Find old cancelled documents\n  const cancelled = await findDocumentsByStatus(\"CANCELLED\");\n  for (const doc of cancelled) {\n    const age = daysSince(new Date(doc.updatedAt!));\n    if (age > RETENTION_POLICY.cancelledDocuments) {\n      await archiveAndDelete(doc.id!);\n      report.cancelledDeleted++;\n    }\n  }\n\n  return report;\n}\n\nfunction daysSince(date: Date): number {\n  const now = new Date();\n  return Math.floor((now.getTime() - date.getTime()) \u002F (1000 * 60 * 60 * 24));\n}\n```\n\n### Step 6: Archive Before Delete\n\n```typescript\ninterface ArchiveRecord {\n  documentId: string;\n  title: string;\n  status: string;\n  completedAt?: string;\n  recipientEmails: string[];  \u002F\u002F Hashed\n  storageKey: string;\n  archivedAt: string;\n}\n\nasync function archiveAndDelete(documentId: string): Promise\u003Cvoid> {\n  const client = getDocumensoClient();\n\n  \u002F\u002F Get document details\n  const doc = await client.documents.getV0({ documentId });\n\n  \u002F\u002F Download PDF if completed\n  let storageKey = \"\";\n  if (doc.status === \"COMPLETED\") {\n    const pdfData = await client.documents.downloadV0({ documentId });\n    const result = await storeSignedDocument(\n      documentId,\n      Buffer.from(pdfData as ArrayBuffer),\n      { title: doc.title!, status: doc.status }\n    );\n    storageKey = result.key;\n  }\n\n  \u002F\u002F Create archive record\n  const archiveRecord: ArchiveRecord = {\n    documentId,\n    title: doc.title!,\n    status: doc.status!,\n    completedAt: doc.completedAt,\n    recipientEmails: doc.recipients?.map((r) =>\n      hashRecipientEmail(r.email!)\n    ) ?? [],\n    storageKey,\n    archivedAt: new Date().toISOString(),\n  };\n\n  \u002F\u002F Save to archive database\n  await db.documentArchive.create({ data: archiveRecord });\n\n  \u002F\u002F Delete from Documenso\n  await client.documents.deleteV0({ documentId });\n\n  console.log(`Archived and deleted document: ${documentId}`);\n}\n```\n\n## GDPR Compliance\n\n### Step 7: Data Subject Access Request (DSAR)\n\n```typescript\ninterface DSARResponse {\n  recipientHash: string;\n  documents: Array\u003C{\n    documentId: string;\n    title: string;\n    status: string;\n    signedAt?: string;\n  }>;\n  exportedAt: string;\n}\n\nasync function handleDSAR(email: string): Promise\u003CDSARResponse> {\n  const recipientHash = hashRecipientEmail(email);\n  const client = getDocumensoClient();\n\n  \u002F\u002F Find all documents associated with this email\n  const documents: DSARResponse[\"documents\"] = [];\n\n  \u002F\u002F Search through documents (pagination required for large datasets)\n  let page = 1;\n  let hasMore = true;\n\n  while (hasMore) {\n    const result = await client.documents.findV0({ page, perPage: 100 });\n\n    for (const doc of result.documents ?? []) {\n      const hasRecipient = doc.recipients?.some(\n        (r) => r.email?.toLowerCase() === email.toLowerCase()\n      );\n\n      if (hasRecipient) {\n        const recipient = doc.recipients?.find(\n          (r) => r.email?.toLowerCase() === email.toLowerCase()\n        );\n\n        documents.push({\n          documentId: doc.id!,\n          title: doc.title!,\n          status: doc.status!,\n          signedAt: recipient?.signedAt,\n        });\n      }\n    }\n\n    hasMore = (result.documents?.length ?? 0) === 100;\n    page++;\n  }\n\n  return {\n    recipientHash,\n    documents,\n    exportedAt: new Date().toISOString(),\n  };\n}\n```\n\n### Step 8: Right to Erasure\n\n```typescript\nasync function handleErasureRequest(email: string): Promise\u003CErasureReport> {\n  const report = {\n    documentsAffected: 0,\n    cannotDelete: [] as string[],\n    deleted: [] as string[],\n  };\n\n  const dsar = await handleDSAR(email);\n\n  for (const doc of dsar.documents) {\n    \u002F\u002F Cannot delete completed documents due to legal requirements\n    if (doc.status === \"COMPLETED\") {\n      report.cannotDelete.push(doc.documentId);\n      console.log(\n        `Cannot delete ${doc.documentId}: Legal retention required`\n      );\n      continue;\n    }\n\n    \u002F\u002F Can delete drafts and cancelled\n    if (doc.status === \"DRAFT\" || doc.status === \"CANCELLED\") {\n      const client = getDocumensoClient();\n      await client.documents.deleteV0({ documentId: doc.documentId });\n      report.deleted.push(doc.documentId);\n    }\n\n    report.documentsAffected++;\n  }\n\n  return report;\n}\n```\n\n## Encryption at Rest\n\n```typescript\nimport crypto from \"crypto\";\n\nconst ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, \"hex\");\n\nfunction encryptPII(plaintext: string): string {\n  const iv = crypto.randomBytes(16);\n  const cipher = crypto.createCipheriv(\"aes-256-gcm\", ENCRYPTION_KEY, iv);\n\n  let encrypted = cipher.update(plaintext, \"utf8\", \"hex\");\n  encrypted += cipher.final(\"hex\");\n\n  const authTag = cipher.getAuthTag().toString(\"hex\");\n\n  return `${iv.toString(\"hex\")}:${authTag}:${encrypted}`;\n}\n\nfunction decryptPII(ciphertext: string): string {\n  const [ivHex, authTagHex, encrypted] = ciphertext.split(\":\");\n\n  const iv = Buffer.from(ivHex, \"hex\");\n  const authTag = Buffer.from(authTagHex, \"hex\");\n\n  const decipher = crypto.createDecipheriv(\"aes-256-gcm\", ENCRYPTION_KEY, iv);\n  decipher.setAuthTag(authTag);\n\n  let decrypted = decipher.update(encrypted, \"hex\", \"utf8\");\n  decrypted += decipher.final(\"utf8\");\n\n  return decrypted;\n}\n```\n\n## Output\n- Signed documents securely stored\n- PII properly protected\n- Retention policies enforced\n- GDPR compliance implemented\n\n## Error Handling\n| Data Issue | Cause | Solution |\n|------------|-------|----------|\n| Download failed | Document not complete | Check status first |\n| Storage failed | Permissions | Check bucket policy |\n| Decryption failed | Wrong key | Verify encryption key |\n| DSAR incomplete | Pagination | Handle all pages |\n\n## Resources\n- [GDPR Requirements](https:\u002F\u002Fgdpr.eu\u002F)\n- [Document Retention Laws](https:\u002F\u002Fwww.nist.gov\u002Fpublications)\n- [AWS S3 Encryption](https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonS3\u002Flatest\u002Fuserguide\u002Fserv-side-encryption.html)\n\n## Next Steps\nFor enterprise RBAC, see `documenso-enterprise-rbac`.\n",{"name":5,"author":55,"license":15,"version":56,"description":57,"allowed-tools":58},"Jeremy Longshore \u003Cjeremy@intentsolutions.io>","1.0.0","Handle document data, signatures, and PII in Documenso integrations.\nUse when managing document lifecycle, handling signed PDFs,\nor implementing data retention policies.\nTrigger with phrases like \"documenso data\", \"signed document\",\n\"document retention\", \"documenso PII\", \"download signed pdf\".\n","Read, Write, Edit","main","90b50ed0778745a80fd5bfb13b1843d45b9e82f7","tests\u002Fext_conformance\u002Fartifacts\u002Fplugins-community\u002Fplugins\u002Fsaas-packs\u002Fdocumenso-pack\u002Fskills\u002Fdocumenso-data-handling",233,"https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust","https:\u002F\u002Fgithub.com\u002FDicklesworthstone","https:\u002F\u002Fwww.jeffreyemanuel.com\u002F","Building in NY","2026-11-15 01:54:15",{"source":8,"skillName":5,"commands":69,"prompt":79,"zipAvailable":80},[70,73,76],{"tool":71,"command":72},"npx","npx skills add Dicklesworthstone\u002Fpi_agent_rust --skill documenso-data-handling",{"tool":74,"command":75},"bunx","bunx skills add Dicklesworthstone\u002Fpi_agent_rust --skill documenso-data-handling",{"tool":77,"command":78},"pnpm","pnpm dlx skills add Dicklesworthstone\u002Fpi_agent_rust --skill documenso-data-handling","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Fplugins-community\u002Fplugins\u002Fsaas-packs\u002Fdocumenso-pack\u002Fskills\u002Fdocumenso-data-handling\u002FSKILL.md",true,[],[83,121,145,170,196,217],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":87,"licenseFileUrl":88,"sourceHtmlUrl":89,"categories":90,"keywords":94,"publishedAt":50},"01M6FEGPPK0ZTKCCM0R3E3KS33","web-artifacts-builder","Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn\u002Fui). Use for complex artifacts requiring state management, routing, or shadcn\u002Fui components - not for simple single-file HTML\u002FJSX artifacts.","Apache-2.0","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Futilities\u002Fweb-artifacts-builder\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Futilities\u002Fweb-artifacts-builder\u002FSKILL.md",[91],{"categoryNo":92,"categoryName":93,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",[95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,41,113,114,115,116,117,118,119,120],"web artifacts builder","web","artifacts","artifacts builder","builder","frontend","css","components","react","component","routing","state management","state","shadcn","file","ui","Tools","Tailwind","html","single-file","simple","ui components","multi","for","jsx","suite",{"skillNo":122,"name":123,"nameZh":6,"description":124,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":87,"licenseFileUrl":125,"sourceHtmlUrl":126,"categories":127,"keywords":132,"publishedAt":50},"01M6FEE9VTK1W4GY21AKREJXRG","webapp-testing","Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fsecurity\u002Fwebapp-testing\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fsecurity\u002Fwebapp-testing\u002FSKILL.md",[128,131],{"categoryNo":129,"categoryName":130,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":92,"categoryName":93,"taxonomyType":21,"taxonomyTypeDesc":22},[133,134,135,100,136,137,96,138,139,140,110,141,142,143,144,35,118],"testing","webapp testing","webapp","screenshots","playwright","debugging","toolkit","browser","logs","applications","behavior","local",{"skillNo":146,"name":147,"nameZh":6,"description":148,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":149,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":50},"01M6FE20F7ZTQBFWKF4MPJ2SBX","neuropixels-analysis","Neuropixels neural recording analysis. Load SpikeGLX\u002FOpenEphys data, preprocess, motion correction, Kilosort4 spike sorting, quality metrics, Allen\u002FIBL curation, AI-assisted visual analysis, for Neuropixels 1.0\u002F2.0 extracellular electrophysiology. Use when working with neural recordings, spike sorting, extracellular electrophysiology, or when the user mentions Neuropixels, SpikeGLX, Open Ephys, Kilosort, quality metrics, or unit curation.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fscientific\u002Fneuropixels-analysis\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fscientific\u002Fneuropixels-analysis\u002FSKILL.md",[],[153,154,155,156,29,157,158,159,41,160,161,162,163,164,165,166,35,167,168,169,118,32],"analysis","neuropixels analysis","spike","metrics","open","quality","user","recordings","Neural","motion","visual","quality metrics","unit","sorting","curation","recording","load",{"skillNo":171,"name":172,"nameZh":6,"description":173,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":87,"licenseFileUrl":174,"sourceHtmlUrl":175,"categories":176,"keywords":180,"publishedAt":50},"01M6FDP0PDK6F1GGX395C7625Z","skill-creator","Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fskill-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fskill-creator\u002FSKILL.md",[177],{"categoryNo":178,"categoryName":179,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[181,182,183,184,185,186,187,188,45,189,190,191,192,193,35,118,194,32,195],"skill creator","creator","workflows","create","update","capabilities","knowledge","tool","specialized","guide","new skill","existing skill","users","Existing","new",{"skillNo":197,"name":198,"nameZh":6,"description":199,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":200,"sourceHtmlUrl":201,"categories":202,"keywords":204,"publishedAt":50},"01M6FDM8Y7VK7NYG9SM7QPA3PW","notebooklm","Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced hallucinations through document-only responses.","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fnotebooklm\u002FLICENSE","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fproductivity\u002Fnotebooklm\u002FSKILL.md",[203],{"categoryNo":178,"categoryName":179,"taxonomyType":21,"taxonomyTypeDesc":22},[198,205,206,207,208,209,210,140,211,212,41,213,40,214,215,118,216],"google","browser automation","gemini","automation","auth","code","library","management","only","query","Citation","source",{"skillNo":218,"name":219,"nameZh":6,"description":220,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":87,"licenseFileUrl":221,"sourceHtmlUrl":222,"categories":223,"keywords":230,"publishedAt":50},"01M6FDHPXE10J3XGTCH5HBHS1M","slack-gif-creator","Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like \"make me a GIF of X doing Y for Slack.\"","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fenterprise-communication\u002Fslack-gif-creator\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002FDicklesworthstone\u002Fpi_agent_rust\u002Fblob\u002Fmain\u002Ftests\u002Fext_conformance\u002Fartifacts\u002Ftemplates-davila7\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fenterprise-communication\u002Fslack-gif-creator\u002FSKILL.md",[224,227],{"categoryNo":225,"categoryName":226,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000005","邮件与沟通",{"categoryNo":228,"categoryName":229,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[231,232,233,182,234,111,235,236,187,41,237,193,238,239,240,118,241,32],"slack gif creator","slack","gif","validation","constraints","Animation","me","make","Utilities","concepts","request"]