Node.js Document Leak Control: Expiring Access Limits, Watermark Evidence
TL;DR: Put an expiring authorization check in front of every contract download, then add a recipient-specific watermark to the bytes that pass that check. Expiry reduces future access; a watermark preserves evidence afte
TL;DR: Put an expiring authorization check in front of every contract download, then add a recipient-specific watermark to the bytes that pass that check. Expiry reduces future access; a watermark preserves evidence after a copy has escaped. Neither mechanism replaces the server-side digital signature or the audit trail.
For a gaming studio sharing talent, publishing, or licensing contracts, the constraint is blunt: access control stops at delivery. Once an authorized recipient has downloaded valid PDF bytes, a timer cannot pull those bytes back. A visible, recipient-bound mark cannot prevent copying either, but it can make screenshots and forwarded files attributable.
Think of the before state as one opaque event: contract.pdf leaves the server. The after state is a chain: authorize the viewer, record the decision, render a viewer-bound copy, preserve the signed original, and record the exact artifact delivered. That chain is the useful control.
Should document leak control use a watermark or expiring access?
An expiring link governs a request, not a file already received. It helps with stale emails, forwarded URLs, and access that should end after a negotiation window. Use a short lifetime, bind the grant to one contract and recipient, reject it server-side after expiry, and record allowed and denied attempts. Do not treat a long random URL as the audit record.
A watermark acts later. Put a stable delivery identifier, recipient label, and issuance time on every page when the contract is rendered. Avoid displaying email addresses or other personal data unless legal and privacy review supports that choice. The delivery identifier can resolve to protected audit data without exposing that data inside the PDF.
That boundary matters.
| Control | Before delivery | After delivery | Audit signal |
|---|---|---|---|
| Expiring authorization | Rejects requests outside the window | Cannot revoke saved bytes | Grant, subject, decision, time |
| Recipient watermark | Does not authorize downloads | Attributes copied pages when the mark survives | Delivery ID and artifact digest |
| PDF digital signature | Detects covered changes | Supports integrity validation | Signer and validation data |
The signature has a different job. PDF 2.0 defines digital-signature structures. Preserve the signed source as an immutable artifact and decide whether watermarking creates a later PDF revision or a separate delivery copy. A graphic pasted onto a page is not a digital signature.
Build the evidence chain in Node.js
This copyable core issues a narrow HMAC-authenticated grant, validates its scope, derives a non-secret delivery ID, calls a PDF stamper, and emits an audit event with a SHA-256 digest of the delivered bytes. The 15-minute window is an explicit example policy, not a universal recommendation.
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
type Grant = {
contractId: string;
recipientId: string;
expiresAt: number;
nonce: string;
};
type AuditEvent = {
action: "contract.delivered";
contractId: string;
recipientId: string;
deliveryId: string;
artifactSha256: string;
occurredAt: string;
};
type StampPdf = (
signedPdf: Uint8Array,
mark: { deliveryId: string; issuedAt: string }
) => Promise<Uint8Array>;
const encode = (value: unknown): string =>
Buffer.from(JSON.stringify(value)).toString("base64url");
function issueGrant(
secret: Uint8Array,
contractId: string,
recipientId: string,
nowMs = Date.now()
): string {
const grant: Grant = {
contractId,
recipientId,
expiresAt: nowMs + 15 * 60 * 1000,
nonce: randomUUID()
};
const payload = encode(grant);
const mac = createHmac("sha256", secret).update(payload).digest("base64url");
return `${payload}.${mac}`;
}
function verifyGrant(
token: string,
secret: Uint8Array,
expectedContractId: string,
expectedRecipientId: string,
nowMs = Date.now()
): Grant {
const [payload, suppliedMac, extra] = token.split(".");
if (!payload || !suppliedMac || extra) throw new Error("Malformed grant");
const expectedMac = createHmac("sha256", secret).update(payload).digest();
const receivedMac = Buffer.from(suppliedMac, "base64url");
if (receivedMac.length !== expectedMac.length ||
!timingSafeEqual(receivedMac, expectedMac)) throw new Error("Invalid grant");
const grant = JSON.parse(
Buffer.from(payload, "base64url").toString("utf8")
) as Grant;
if (grant.contractId !== expectedContractId ||
grant.recipientId !== expectedRecipientId) throw new Error("Grant scope mismatch");
if (!Number.isSafeInteger(grant.expiresAt) || nowMs >= grant.expiresAt) {
throw new Error("Grant expired");
}
return grant;
}
async function deliverContract(
signedPdf: Uint8Array,
grant: Grant,
stampPdf: StampPdf,
appendAudit: (event: AuditEvent) => Promise<void>,
now = new Date()
): Promise<Uint8Array> {
const occurredAt = now.toISOString();
const deliveryId = createHash("sha256")
.update(`${grant.contractId}:${grant.recipientId}:${grant.nonce}`)
.digest("hex").slice(0, 20);
const deliveredPdf = await stampPdf(signedPdf, { deliveryId, issuedAt: occurredAt });
const artifactSha256 = createHash("sha256").update(deliveredPdf).digest("hex");
await appendAudit({
action: "contract.delivered",
contractId: grant.contractId,
recipientId: grant.recipientId,
deliveryId,
artifactSha256,
occurredAt
});
return deliveredPdf;
}
Keep the secret in a managed secret store and support rotation. The example leaves stampPdf and durable audit storage behind interfaces because PDF signature preservation depends on how signed revisions are constructed, while audit durability depends on the threat model. Those details deserve explicit tests.
One ordering choice matters. If the audit write fails, do not return the file. Otherwise the studio can deliver a contract with no corresponding evidence. Record rejected decisions separately, with enough context to investigate abuse but without logging the token.
Observe the decision, not the document contents
A useful dashboard follows the chain in words: grant issued, request evaluated, watermark rendered, artifact hashed, audit committed, bytes returned. Counters for allowed, expired, invalid, and scope-mismatch decisions reveal different failure modes. A latency histogram around rendering and audit persistence shows where downloads slow down. Alert on a sustained rise in denied decisions or audit-write failures; one rejected expired link is normal behavior. Keep contract text, bearer grants, signing keys, and raw personal data out of logs. Correlation needs identifiers, not secrets. For each successful delivery, the contract ID, recipient pseudonym, delivery ID, artifact digest, decision time, and policy version can reconstruct the server's action. Protect the audit store with its own access controls and retention policy. Test the awkward paths: advance a fake clock to the exact expiry boundary, try a valid grant against the wrong recipient and contract, and confirm that two recipients receive different delivery IDs. A repeated delivery must be separately observable, and an audit failure must prevent the response. Then validate the resulting PDF and its signature behavior with an independent verifier appropriate to the signing profile.
No audit, no download.
What if the recipient crops or removes the watermark?
Assume a motivated recipient can alter visible pixels. Put the identifier on every page, place it where routine cropping is costly, and keep the artifact digest in the protected audit record. This raises the effort and preserves an evidentiary link when the mark survives; it does not provide cryptographic proof that a named person caused a leak.
There is a real trade-off. Aggressive marks can obstruct signatures or contract text, while faint marks may disappear in screenshots or printouts. Test real studio documents on desktop, mobile, print, and grayscale output. Legal and privacy reviewers should approve displayed fields and retention before launch.
Could PDF encryption permissions solve this instead? Viewer controls around copying or printing still depend on the reader and key handling. Treat those permissions as another layer, never as a reason to omit request-time authorization, traceable rendering, or the immutable signed source.
The operating rule is compact. Use expiry when the question is, "May this recipient fetch this contract now?" Use a watermark when the question is, "Which delivered copy are we looking at?" Use a digital signature when the question is, "Has the signed PDF revision changed, and can its signature be validated?"
For gaming contract sharing, the practical design asks all three questions. The server authorizes a narrow request, produces a traceable delivery copy without losing the signed original, and commits evidence before returning bytes. This does not promise leak prevention after download. It gives the team bounded access before delivery and a defensible investigation path afterward.
References
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.