docforensics-client
v0.1.1
Published
Node.js / TypeScript client for the TraceDocuments document forensics B2B API. Typed REST client with HMAC webhook verification helpers for PDF fingerprinting, watermarking and leak detection.
Downloads
118
Maintainers
Readme
docforensics-client
Node.js / TypeScript client for the TraceDocuments B2B Enterprise API — document forensics: PDF fingerprinting, watermarking, and leak detection.
- ✅ Zero runtime dependencies (uses native
fetchandnode:crypto) - ✅ First-class TypeScript types for every endpoint and webhook payload
- ✅ Webhook signature verification helper (HMAC-SHA256, timing-safe)
- ✅ Strongly-typed errors (
UnauthorizedError,RateLimitError, …) - ✅ Works with Node.js ≥ 18, Bun, Deno, Cloudflare Workers, Vercel Edge
Install
npm install docforensics-client
# or
pnpm add docforensics-client
# or
yarn add docforensics-clientQuick start
import { TraceDocumentsClient } from 'docforensics-client';
const td = new TraceDocumentsClient({ apiKey: process.env.TD_API_KEY! });
// Protect a PDF
const pdfBase64 = Buffer.from(await fs.readFile('contract.pdf')).toString('base64');
const protectResult = await td.protectDocument({
pdfBase64,
embedderTier: 'standard',
customMetadata: 'contract-2026-001',
});
console.log(protectResult.documentId, protectResult.fingerprintHash);
// Verify a (possibly leaked) PDF
const verify = await td.verifyDocument({ pdfBase64: leakedPdfBase64 });
if (verify.integrityStatus === 'tampered') {
console.warn('Document was modified after protection!');
}Webhooks
When you register a webhook with td.registerWebhook(...), you receive a one-time signingSecret.
Store it (e.g. in your secrets manager) and use it to validate every incoming POST.
import { parseWebhookRequest } from 'docforensics-client';
import express from 'express';
const app = express();
// IMPORTANT: capture the raw request body — JSON.parse + re-stringify breaks the signature.
app.post('/webhooks/tracedocuments',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const { event, payload } = parseWebhookRequest({
payload: req.body, // Buffer
headers: req.headers, // Express provides lowercase keys
secret: process.env.TD_WEBHOOK_SECRET!,
});
if (event === 'leak_detected') {
console.log('Leak detected:', payload.documentId, payload.leakSource);
} else if (event === 'document_verified') {
console.log('Verify event:', payload.documentId, payload.riskLevel);
}
res.sendStatus(200);
} catch (err) {
res.status(401).send('Invalid signature');
}
},
);Manual signature check
If you prefer to handle headers yourself:
import { verifyWebhookSignature } from 'docforensics-client';
verifyWebhookSignature({
payload: rawBody, // string or Buffer
signature: req.headers['x-tracedocuments-signature'],
secret: process.env.TD_WEBHOOK_SECRET!,
});
// throws WebhookSignatureError on mismatchThe signature is computed as sha256=<lowercase-hex> over the raw POST body using HMAC-SHA256.
Comparison is done in constant time via crypto.timingSafeEqual.
Errors
Every API failure throws a typed subclass of TraceDocumentsError:
| Class | Status | When |
|---|---|---|
| UnauthorizedError | 401 | Missing or invalid API key |
| RateLimitError | 429 | Hourly quota exceeded |
| ApiValidationError | 4xx | Other client errors (carries .message from API) |
| ApiServerError | 5xx | Server-side failure |
| WebhookSignatureError | — | Signature verification failed |
| TraceDocumentsError | — | Network errors / timeouts / configuration |
import { RateLimitError } from 'docforensics-client';
try {
await td.verifyDocument({ pdfBase64 });
} catch (err) {
if (err instanceof RateLimitError) await sleep(60_000);
else throw err;
}API surface
| Method | Endpoint |
|---|---|
| protectDocument(req) | POST /api/b2b/protect |
| verifyDocument(req) | POST /api/b2b/verify |
| batchProtect(req) | POST /api/b2b/batch/protect |
| getDocument(id) | GET /api/b2b/documents/{id} |
| listDocuments(params) | GET /api/b2b/documents |
| getRateLimitStatus() | GET /api/b2b/status/ratelimit |
| getQuotaStatus() | GET /api/b2b/status/quota |
| getUsageSummary(params?) | GET /api/b2b/usage/summary |
| exportUsage(params?) | GET /api/b2b/usage/export?format=csv (returns CSV string) |
| registerWebhook(req) | POST /api/b2b/webhooks |
| listWebhooks() | GET /api/b2b/webhooks |
| deleteWebhook(id) | DELETE /api/b2b/webhooks/{id} |
Sandbox
Use the public sandbox key td_sandbox_demo to prototype your integration without an
account, real credits, or persistent state. Every endpoint returns a deterministic canned
payload — perfect for CI, local dev, and recorded demos.
const td = new TraceDocumentsClient({
apiKey: 'td_sandbox_demo',
// baseUrl: 'https://api.tracedocuments.com', // default
});
const r = await td.protectDocument({ pdfBase64: 'AAAA', embedderTier: 'standard' });
// → r.documentId === 'doc_sandbox_demo_0001'
// → r.fingerprintHash === '9d97373b…' (always the same)
// Pass an `expectedFingerprintHash` that doesn't match to simulate a tampered doc:
const tampered = await td.verifyDocument({
pdfBase64: 'BBBB',
expectedFingerprintHash: 'deadbeef',
});
// → tampered.integrityStatus === 'tampered'
// → tampered.riskLevel === 'high'Sandbox webhooks are accepted but never delivered. The signing secret returned on
registerWebhook (whsec_sandbox_demo_NEVER_USED_FOR_REAL_DELIVERIES) is a static
sentinel and will never sign a real payload.
Testing
The SDK ships with a Vitest suite (npm test) that includes a contract-level E2E
suite (src/__tests__/sandbox.e2e.test.ts) which spins up an in-process HTTP
server mirroring the public sandbox responses and exercises every client method
via real fetch. If a future SDK or backend change drifts away from the
B2bSandbox contract, that suite breaks.
cd packages/sdk-node
npm install
npm run typecheck # tsc --noEmit
npm test # 45 tests across unit + webhook + sandbox E2E
npm run build # tsc -p tsconfig.build.json → dist/License
MIT
