๐Ÿ“… let's chat! explore the endless possibilities creating industries that don't exist. click here

documenso-security-basics

Implement security best practices for Documenso document signing integrations. Use when securing API keys, configuring webhooks securely, or implementing document security measures. Trigger with phrases like "documenso security", "secure documenso", "documenso API key security", "documenso webhook security". allowed-tools: Read, Write, Edit version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>

Allowed Tools

No tools specified

Provided by Plugin

documenso-pack

Claude Code skill pack for Documenso (24 skills)

saas packs v1.0.0
View Plugin

Installation

This skill is included in the documenso-pack plugin:

/plugin install documenso-pack@claude-code-plugins-plus

Click to copy

Instructions

# Documenso Security Basics ## Overview Essential security practices for Documenso integrations including API key management, webhook security, and document protection. ## Prerequisites - Documenso account with API access - Understanding of environment variables - Basic security concepts ## Instructions ### Step 1: Secure API Key Management ```typescript // NEVER do this: const client = new Documenso({ apiKey: "dcs_abc123...", // Hardcoded - BAD! }); // ALWAYS do this: const client = new Documenso({ apiKey: process.env.DOCUMENSO_API_KEY ?? "", }); // Validate API key is present at startup function validateEnvironment(): void { const required = ["DOCUMENSO_API_KEY"]; const missing = required.filter((key) => !process.env[key]); if (missing.length > 0) { throw new Error( `Missing required environment variables: ${missing.join(", ")}` ); } // Validate API key format const apiKey = process.env.DOCUMENSO_API_KEY!; if (!apiKey.startsWith("dcs_")) { console.warn("Warning: API key format unexpected (should start with dcs_)"); } } ``` ### Step 2: API Key Rotation ```typescript // Support multiple API keys for rotation interface KeyRotationConfig { primaryKey: string; secondaryKey?: string; // Fallback during rotation } async function getClientWithFallback( config: KeyRotationConfig ): Promise { // Try primary key first try { const client = new Documenso({ apiKey: config.primaryKey }); await client.documents.findV0({ perPage: 1 }); return client; } catch (error: any) { if (error.statusCode === 401 && config.secondaryKey) { console.warn("Primary key failed, trying secondary..."); return new Documenso({ apiKey: config.secondaryKey }); } throw error; } } // Rotation procedure: // 1. Generate new key in Documenso dashboard // 2. Set as DOCUMENSO_API_KEY_SECONDARY // 3. Test secondary key works // 4. Swap: SECONDARY -> PRIMARY // 5. Revoke old primary key // 6. Remove secondary env var ``` ### Step 3: Webhook Security ```typescript import express from "express"; const app = express(); // Parse raw body for signature verification app.use("/webhooks/documenso", express.raw({ type: "application/json" })); app.post("/webhooks/documenso", (req, res) => { // Verify webhook secret const receivedSecret = req.headers["x-documenso-secret"]; const expectedSecret = process.env.DOCUMENSO_WEBHOOK_SECRET; if (!expectedSecret) { console.error("DOCUMENSO_WEBHOOK_SECRET not configured"); return res.status(500).json({ error: "Webhook not configured" }); } // Timing-safe comparison to prevent timing attacks if (!timingSafeEqual(receivedSecret as string, expectedSecret)) { console.warn("Invalid webhook secret received"); return res.status(401).json({ error: "Invalid signature" }); } // Parse and process try { const payload = JSON.parse(req.body.toString()); handleWebhookEvent(payload); res.status(200).json({ received: true }); } catch (error) { console.error("Webhook processing error:", error); res.status(400).json({ error: "Invalid payload" }); } }); function timingSafeEqual(a: string, b: string): boolean { if (a.length !== b.length) return false; const bufA = Buffer.from(a); const bufB = Buffer.from(b); return require("crypto").timingSafeEqual(bufA, bufB); } ``` ### Step 4: Document Access Control ```typescript // Track document ownership for access control interface DocumentAccess { documentId: string; ownerId: string; authorizedEmails: string[]; } class DocumentAccessControl { private accessMap = new Map(); async createDocument( userId: string, title: string, authorizedEmails: string[] ): Promise { const doc = await client.documents.createV0({ title }); const documentId = doc.documentId!; this.accessMap.set(documentId, { documentId, ownerId: userId, authorizedEmails, }); return documentId; } canAccess(userId: string, documentId: string): boolean { const access = this.accessMap.get(documentId); if (!access) return false; return access.ownerId === userId; } canSign(email: string, documentId: string): boolean { const access = this.accessMap.get(documentId); if (!access) return false; return access.authorizedEmails.includes(email.toLowerCase()); } } ``` ### Step 5: Signing URL Security ```typescript // Signing URLs should be treated as sensitive // They grant access to sign documents // DON'T expose signing URLs in logs function logDocument(doc: any): void { const safeDoc = { ...doc }; if (safeDoc.recipients) { safeDoc.recipients = safeDoc.recipients.map((r: any) => ({ ...r, signingUrl: "[REDACTED]", signingToken: "[REDACTED]", })); } console.log(JSON.stringify(safeDoc, null, 2)); } // DON'T store signing URLs in insecure locations // They should be sent directly to recipients via secure channel // DO set appropriate expiry on embedded signing sessions async function getSecureSigningSession( documentId: string, recipientEmail: string ): Promise<{ signingUrl: string; expiresAt: Date }> { const doc = await client.documents.getV0({ documentId }); const recipient = doc.recipients?.find((r) => r.email === recipientEmail); if (!recipient?.signingUrl) { throw new Error("Signing URL not available"); } return { signingUrl: recipient.signingUrl, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours }; } ``` ### Step 6: Input Validation ```typescript import { z } from "zod"; // Validate recipient input const RecipientInputSchema = z.object({ email: z .string() .email("Invalid email format") .transform((e) => e.toLowerCase().trim()), name: z .string() .min(1, "Name is required") .max(100, "Name too long") .transform((n) => n.trim()), role: z.enum(["SIGNER", "APPROVER", "VIEWER", "CC"]), }); // Validate document title const DocumentInputSchema = z.object({ title: z .string() .min(1, "Title is required") .max(255, "Title too long") .refine( (t) => !/