| name | documenso-security-basics |
| description | 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> |
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
const client = new Documenso({
apiKey: "dcs_abc123...",
});
const client = new Documenso({
apiKey: process.env.DOCUMENSO_API_KEY ?? "",
});
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(", ")}`
);
}
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
interface KeyRotationConfig {
primaryKey: string;
secondaryKey?: string;
}
async function getClientWithFallback(
config: KeyRotationConfig
): Promise<Documenso> {
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;
}
}
Step 3: Webhook Security
import express from "express";
const app = express();
app.use("/webhooks/documenso", express.raw({ type: "application/json" }));
app.post("/webhooks/documenso", (req, res) => {
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" });
}
if (!timingSafeEqual(receivedSecret as string, expectedSecret)) {
console.warn("Invalid webhook secret received");
return res.status(401).json({ error: "Invalid signature" });
}
{
payload = .(req..());
(payload);
res.().({ : });
} (error) {
.(, error);
res.().({ : });
}
});
(): {
(a. !== b.) ;
bufA = .(a);
bufB = .(b);
().(bufA, bufB);
}
Step 4: Document Access Control
interface DocumentAccess {
documentId: string;
ownerId: string;
authorizedEmails: string[];
}
class DocumentAccessControl {
private accessMap = new Map<string, DocumentAccess>();
async createDocument(
userId: string,
title: string,
authorizedEmails: string[]
): Promise<string> {
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 ;
access. === userId;
}
(: , : ): {
access = ..(documentId);
(!access) ;
access..(email.());
}
}
Step 5: Signing URL Security
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));
}
async function getSecureSigningSession(
documentId: string,
recipientEmail: string
): Promise<{ signingUrl: string; expiresAt: Date }> {
const doc = await client.documents.getV0({ documentId });
recipient = doc.?.( r. === recipientEmail);
(!recipient?.) {
();
}
{
: recipient.,
: (.() + * * * ),
};
}
Step 6: Input Validation
import { z } from "zod";
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"]),
});
const DocumentInputSchema = z.object({
title: z
.string()
.min(1, "Title is required")
.max(255, "Title too long")
.refine(
(t) => !/<script/i.test(t),
),
});
(): <> {
maxBytes = maxSizeMb * * ;
(file. > maxBytes) {
();
}
pdfMagic = .([, , , ]);
(!file.(, ).(pdfMagic)) {
();
}
;
}
() {
validated = .(input);
client..(validated);
}
Step 7: Audit Logging
interface AuditEntry {
timestamp: Date;
userId: string;
action: string;
resourceType: "document" | "template" | "recipient";
resourceId: string;
ipAddress?: string;
userAgent?: string;
success: boolean;
error?: string;
}
class AuditLogger {
private entries: AuditEntry[] = [];
log(entry: Omit<AuditEntry, "timestamp">): void {
const fullEntry: AuditEntry = {
...entry,
timestamp: new Date(),
};
this.entries.push(fullEntry);
console.log(
`[AUDIT] ${entry.action} ${entry.resourceType}:${entry.resourceId} ` +
`by ${entry.userId} - ${entry.success ? : }`
);
(!entry. && entry. === ) {
.();
}
}
(limit = ): [] {
..(-limit);
}
}
auditLogger = ();
(): <> {
{
client..({ documentId });
auditLogger.({
userId,
: ,
: ,
: documentId,
: ,
});
;
} (: ) {
auditLogger.({
userId,
: ,
: ,
: documentId,
: ,
: error.,
});
error;
}
}
Security Checklist
Output
- Secure API key management
- Validated webhook endpoints
- Input sanitization
- Audit trail for compliance
Error Handling
| Security Issue | Indicator | Response |
|---|
| Invalid API key | 401 errors | Rotate key |
| Webhook spoofing | Invalid secret | Reject and alert |
| Unauthorized access | 403 errors | Check permissions |
| Brute force | Many 401s | Rate limit IP |
Resources
Next Steps
For production deployment, see documenso-prod-checklist.