platform-esigning
Provider-agnostic e-signing infrastructure for NestJS. Exposes a unified EsigningClientPort abstraction with an internal provider out of the box and prepared entry points for DocuSign, Adobe Sign, Dropbox Sign, and signNow. Optionally tracks signing request state via the EsigningPersistencePort.
Package: @breadstone/archipel-platform-esigning
Architecture
Solid lines indicate shipped implementations. Dashed lines indicate provider entry points prepared for implementation. Each provider SDK is an optional peer dependency — install only the one you need.
Quick Start
1. Register with the internal provider
import { Module } from '@nestjs/common';
import { EsigningModule, InternalEsigningProvider } from '@breadstone/archipel-platform-esigning';
@Module({
imports: [
EsigningModule.register({
provider: InternalEsigningProvider,
}),
],
})
export class AppModule {}2. Register with an external provider
import { Module } from '@nestjs/common';
import { EsigningModule } from '@breadstone/archipel-platform-esigning';
import { DOCUSIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/docusign';
@Module({
imports: [
EsigningModule.register({
provider: DocuSignClient, // your EsigningClientPort implementation
configEntries: DOCUSIGN_CONFIG_ENTRIES,
persistence: PrismaEsigningAdapter, // optional
isGlobal: false,
}),
],
})
export class AppModule {}Switching providers
Replace the imports — no other code change needed:
import { ADOBE_SIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/adobe-sign';
EsigningModule.register({
provider: AdobeSignClient,
configEntries: ADOBE_SIGN_CONFIG_ENTRIES,
});Module Configuration
IEsigningModuleOptions
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
provider | Type<EsigningClientPort> | Yes | — | Provider implementation (e.g. InternalEsigningProvider) |
configEntries | ReadonlyArray<Omit<IConfigRegistryEntry, 'module'>> | No | [] | Provider-specific config entries |
persistence | Type<EsigningPersistencePort> | No | — | Signing request state tracking (e.g. Prisma adapter) |
isGlobal | boolean | No | false | Register as a global module |
E-Signing Providers
Internal (built-in)
The InternalEsigningProvider manages signing requests in-memory. Suitable for development, testing, or simple self-managed signing workflows.
import { InternalEsigningProvider } from '@breadstone/archipel-platform-esigning';
EsigningModule.register({
provider: InternalEsigningProvider,
});Internal Provider
The internal provider also exposes a completeSigner() method that allows the consuming application to programmatically complete a signer's step — useful for self-hosted signing UIs.
DocuSign
Subpath: @breadstone/archipel-platform-esigning/docusign
| Variable | Description | Required |
|---|---|---|
DOCUSIGN_INTEGRATION_KEY | DocuSign integration key (client ID) | Yes |
DOCUSIGN_SECRET_KEY | DocuSign secret key (client secret) | Yes |
DOCUSIGN_ACCOUNT_ID | DocuSign account ID | Yes |
DOCUSIGN_BASE_URL | DocuSign base URL (e.g. https://demo.docusign.net) | Yes |
DOCUSIGN_WEBHOOK_HMAC_KEY | DocuSign webhook HMAC key | No |
import { DOCUSIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/docusign';Adobe Sign
Subpath: @breadstone/archipel-platform-esigning/adobe-sign
| Variable | Description | Required |
|---|---|---|
ADOBE_SIGN_INTEGRATION_KEY | Adobe Sign integration key | Yes |
ADOBE_SIGN_CLIENT_SECRET | Adobe Sign client secret | Yes |
ADOBE_SIGN_BASE_URL | Adobe Sign base URL | Yes |
ADOBE_SIGN_WEBHOOK_CLIENT_ID | Adobe Sign webhook client ID for verification | No |
import { ADOBE_SIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/adobe-sign';Dropbox Sign
Subpath: @breadstone/archipel-platform-esigning/dropbox-sign
| Variable | Description | Required |
|---|---|---|
DROPBOX_SIGN_API_KEY | Dropbox Sign API key | Yes |
DROPBOX_SIGN_CLIENT_ID | Dropbox Sign client ID | Yes |
DROPBOX_SIGN_WEBHOOK_SECRET | Dropbox Sign webhook secret | No |
import { DROPBOX_SIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/dropbox-sign';signNow
Subpath: @breadstone/archipel-platform-esigning/signnow
| Variable | Description | Required |
|---|---|---|
SIGNNOW_API_KEY | signNow API key (basic token) | Yes |
SIGNNOW_CLIENT_ID | signNow client ID | Yes |
SIGNNOW_CLIENT_SECRET | signNow client secret | Yes |
SIGNNOW_BASE_URL | signNow base URL | Yes |
SIGNNOW_WEBHOOK_SECRET | signNow webhook callback secret | No |
import { SIGNNOW_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/signnow';EsigningClientPort
All providers implement this abstract contract. Inject EsigningClientPort in services to stay provider-agnostic:
abstract class EsigningClientPort {
abstract readonly providerId: string;
abstract createSigningRequest(request: ICreateSigningRequest): Promise<ISigningRequest>;
abstract createSigningSession(request: ICreateSigningSessionRequest): Promise<ISigningSession>;
abstract getSigningRequest(signingRequestId: string): Promise<ISigningRequest>;
abstract getSignedDocuments(signingRequestId: string): Promise<Array<ISignedDocument>>;
abstract cancelSigningRequest(signingRequestId: string, reason?: string): Promise<void>;
abstract verifyWebhook(payload: string | Buffer, signature: string): Promise<IEsigningWebhookEvent>;
}Usage in services
import { EsigningClientPort } from '@breadstone/archipel-platform-esigning';
@Injectable()
export class ContractService {
constructor(private readonly _esigning: EsigningClientPort) {}
public async sendForSigning(contract: Contract): Promise<string> {
const signingRequest = await this._esigning.createSigningRequest({
subject: `Contract: ${contract.title}`,
documents: [
{
name: 'contract.pdf',
contentType: 'application/pdf',
content: contract.pdfBuffer,
order: 1,
},
],
signers: [
{
email: contract.signerEmail,
name: contract.signerName,
role: 'signer',
order: 1,
},
],
});
return signingRequest.signingRequestId;
}
}Create Signing Request
const signingRequest = await this._esigning.createSigningRequest({
subject: 'NDA Agreement',
message: 'Please sign this NDA at your earliest convenience.',
documents: [{ name: 'nda.pdf', contentType: 'application/pdf', content: pdfBuffer, order: 1 }],
signers: [{ email: 'alice@example.com', name: 'Alice', role: 'signer', order: 1 }],
fields: [
{
type: 'signature',
signerId: '...', // resolved after creation
documentId: '...',
pageNumber: 1,
positionX: 100,
positionY: 500,
width: 200,
height: 50,
required: true,
},
],
expiresInHours: 72,
});
// → ISigningRequestCreate Signing Session
const session = await this._esigning.createSigningSession({
signingRequestId: 'sr_abc123',
signerId: 'sgn_xyz789',
returnUrl: 'https://app.example.com/signing/complete',
});
// → ISigningSession { signingUrl, expiresAt, ... }Get Signing Request Status
const request = await this._esigning.getSigningRequest('sr_abc123');
// → ISigningRequest { status, signers, documents, ... }Get Signed Documents
const documents = await this._esigning.getSignedDocuments('sr_abc123');
// → ISignedDocument[] { documentId, content, contentType, sizeInBytes, ... }Cancel Signing Request
await this._esigning.cancelSigningRequest('sr_abc123', 'Contract terms changed.');Verify Webhook
const event = await this._esigning.verifyWebhook(rawBody, signatureHeader);
// → IEsigningWebhookEvent { eventType, signingRequestId, ... }EsigningService
The EsigningService wraps EsigningClientPort and adds optional persistence tracking via EsigningPersistencePort:
import { EsigningService } from '@breadstone/archipel-platform-esigning';
@Injectable()
export class ContractWorkflowService {
constructor(private readonly _esigning: EsigningService) {}
public async initiateSigningFlow(contractId: string): Promise<ISigningRequest> {
// Automatically persists via EsigningPersistencePort (if registered)
return this._esigning.createSigningRequest({ ... });
}
}All methods on EsigningService mirror EsigningClientPort and additionally invoke the persistence port when registered:
| Method | Persistence Hook |
|---|---|
createSigningRequest | onSigningRequestCreated() |
cancelSigningRequest | onSigningRequestCancelled() |
verifyWebhook | onSigningRequestStatusChanged() |
createSigningSession | — |
getSigningRequest | — |
getSignedDocuments | — |
Ports (Contracts)
EsigningPersistencePort
Tracks signing request state changes. Optional — when not provided, signing still works but state is not persisted.
abstract class EsigningPersistencePort {
abstract onSigningRequestCreated(signingRequest: ISigningRequest): Promise<void>;
abstract onSigningRequestStatusChanged(signingRequestId: string, status: string): Promise<void>;
abstract onSigningRequestCancelled(signingRequestId: string): Promise<void>;
}Real-World Adapter: Prisma
@Injectable()
export class PrismaEsigningAdapter extends EsigningPersistencePort {
private readonly _prisma: PrismaService;
constructor(prisma: PrismaService) {
super();
this._prisma = prisma;
}
public async onSigningRequestCreated(signingRequest: ISigningRequest): Promise<void> {
await this._prisma.signingRequest.create({
data: {
externalId: signingRequest.signingRequestId,
provider: 'docusign',
subject: signingRequest.subject,
status: signingRequest.status,
createdAt: signingRequest.createdAt,
},
});
}
public async onSigningRequestStatusChanged(signingRequestId: string, status: string): Promise<void> {
await this._prisma.signingRequest.update({
where: { externalId: signingRequestId },
data: { status, updatedAt: new Date() },
});
}
public async onSigningRequestCancelled(signingRequestId: string): Promise<void> {
await this._prisma.signingRequest.update({
where: { externalId: signingRequestId },
data: { status: 'cancelled', cancelledAt: new Date() },
});
}
}Normalized Types
All providers map their native API responses into these shared interfaces.
ISigningRequest
| Field | Type | Description |
|---|---|---|
signingRequestId | string | Provider-specific signing request identifier |
status | SigningRequestStatus | Current lifecycle status |
subject | string | Subject or title |
message | string? | Optional message for signers |
signers | ReadonlyArray<ISigner> | Participating signers |
documents | ReadonlyArray<ISigningDocument> | Included documents |
fields | ReadonlyArray<ISigningField> | Fields across documents |
createdAt | Date | Creation timestamp |
updatedAt | Date | Last update timestamp |
expiresAt | Date? | Expiration timestamp |
providerMetadata | Record<string, unknown>? | Provider-specific raw metadata |
ISigner
| Field | Type | Description |
|---|---|---|
signerId | string | Provider-specific signer identifier |
email | string | Email address |
name | string | Display name |
role | SignerRole | Role in the signing process |
status | SignerStatus | Current status |
order | number | Routing order (signing sequence) |
ISigningDocument
| Field | Type | Description |
|---|---|---|
documentId | string | Provider-specific document identifier |
name | string | Display name |
contentType | string | MIME type |
sizeInBytes | number? | File size in bytes |
pageCount | number? | Number of pages |
order | number | Order within the signing request |
ISigningField
| Field | Type | Description |
|---|---|---|
fieldId | string | Provider-specific field identifier |
type | SigningFieldType | Field type (signature, initials, etc.) |
signerId | string | Associated signer identifier |
documentId | string | Document this field is placed on |
pageNumber | number | Page number (1-based) |
positionX | number | X-coordinate in points |
positionY | number | Y-coordinate in points |
width | number | Width in points |
height | number | Height in points |
required | boolean | Whether the field is required |
value | string? | Current value, if filled |
ISignedDocument
| Field | Type | Description |
|---|---|---|
documentId | string | Provider-specific document ID |
signingRequestId | string | Parent signing request ID |
name | string | Display name |
contentType | string | MIME type |
content | Buffer | Signed document content |
sizeInBytes | number | File size in bytes |
ISigningSession
| Field | Type | Description |
|---|---|---|
sessionId | string | Session identifier |
signingRequestId | string | Parent signing request ID |
signerId | string | Signer this session belongs to |
signingUrl | string | URL to redirect signer to |
expiresAt | Date | Session expiration timestamp |
IEsigningWebhookEvent
| Field | Type | Description |
|---|---|---|
eventId | string | Provider-specific event ID |
eventType | WebhookEventType | Normalized event type |
signingRequestId | string | Related signing request ID |
signerId | string? | Related signer ID (if applicable) |
occurredAt | Date | Event timestamp |
rawPayload | unknown | Provider-specific raw payload |
Status & Role Enums
SigningRequestStatus
const SigningRequestStatuses = {
Draft: 'draft',
Sent: 'sent',
InProgress: 'in-progress',
Completed: 'completed',
Declined: 'declined',
Cancelled: 'cancelled',
Expired: 'expired',
Voided: 'voided',
} as const;
type SigningRequestStatus =
| 'draft'
| 'sent'
| 'in-progress'
| 'completed'
| 'declined'
| 'cancelled'
| 'expired'
| 'voided';SignerStatus
const SignerStatuses = {
Pending: 'pending',
Sent: 'sent',
Delivered: 'delivered',
Signed: 'signed',
Declined: 'declined',
Expired: 'expired',
} as const;
type SignerStatus = 'pending' | 'sent' | 'delivered' | 'signed' | 'declined' | 'expired';SignerRole
const SignerRoles = {
Signer: 'signer',
CarbonCopy: 'carbon-copy',
InPersonSigner: 'in-person-signer',
Approver: 'approver',
Witness: 'witness',
} as const;
type SignerRole = 'signer' | 'carbon-copy' | 'in-person-signer' | 'approver' | 'witness';SigningFieldType
const SigningFieldTypes = {
Signature: 'signature',
Initials: 'initials',
DateSigned: 'date-signed',
Text: 'text',
Checkbox: 'checkbox',
Name: 'name',
Email: 'email',
Company: 'company',
Title: 'title',
} as const;
type SigningFieldType =
| 'signature'
| 'initials'
| 'date-signed'
| 'text'
| 'checkbox'
| 'name'
| 'email'
| 'company'
| 'title';WebhookEventType
const WebhookEventTypes = {
SigningRequestCompleted: 'signing-request.completed',
SigningRequestDeclined: 'signing-request.declined',
SigningRequestVoided: 'signing-request.voided',
SigningRequestExpired: 'signing-request.expired',
SigningRequestSent: 'signing-request.sent',
SignerCompleted: 'signer.completed',
SignerDeclined: 'signer.declined',
SignerSent: 'signer.sent',
SignerDelivered: 'signer.delivered',
} as const;Error Classes
| Error | Code | Description |
|---|---|---|
EsigningError | Custom code property | Base error for all e-signing failures |
SigningProviderError | SIGNING_PROVIDER_ERROR | Provider-level operation failed |
SigningRequestNotFoundError | SIGNING_REQUEST_NOT_FOUND | Signing request not found |
WebhookVerificationError | WEBHOOK_VERIFICATION_FAILED | Webhook signature verification failed |
import {
EsigningError,
SigningProviderError,
SigningRequestNotFoundError,
WebhookVerificationError,
} from '@breadstone/archipel-platform-esigning';Implementing a Custom Provider
To implement a new e-signing provider, extend EsigningClientPort:
import { Injectable } from '@nestjs/common';
import { EsigningClientPort } from '@breadstone/archipel-platform-esigning';
import type {
ICreateSigningRequest,
ICreateSigningSessionRequest,
IEsigningWebhookEvent,
ISignedDocument,
ISigningRequest,
ISigningSession,
} from '@breadstone/archipel-platform-esigning';
@Injectable()
export class DocuSignClient extends EsigningClientPort {
public readonly providerId = 'docusign';
public async createSigningRequest(request: ICreateSigningRequest): Promise<ISigningRequest> {
// DocuSign eSignature REST API implementation
}
public async createSigningSession(request: ICreateSigningSessionRequest): Promise<ISigningSession> {
// Create recipient view URL
}
public async getSigningRequest(signingRequestId: string): Promise<ISigningRequest> {
// Get envelope status
}
public async getSignedDocuments(signingRequestId: string): Promise<Array<ISignedDocument>> {
// Download completed documents
}
public async cancelSigningRequest(signingRequestId: string, reason?: string): Promise<void> {
// Void envelope
}
public async verifyWebhook(payload: string | Buffer, signature: string): Promise<IEsigningWebhookEvent> {
// Verify HMAC and normalize Connect event
}
}
// Register
EsigningModule.register({
provider: DocuSignClient,
configEntries: DOCUSIGN_CONFIG_ENTRIES,
});Exports Summary
| Export | Type | Description |
|---|---|---|
EsigningModule | NestJS Module | Main module with register() |
EsigningService | Service | Application service wrapping provider + persistence |
EsigningClientPort | Port | Abstract provider contract |
EsigningPersistencePort | Port | Optional state tracking contract |
InternalEsigningProvider | Provider | Built-in in-memory implementation |
IEsigningModuleOptions | Interface | Module configuration options |
ISigningRequest | Interface | Signing request model |
ISigningSession | Interface | Signing session/link model |
ISignedDocument | Interface | Signed document model |
ISigner | Interface | Signer model |
ISigningDocument | Interface | Document model |
ISigningField | Interface | Field definition model |
IEsigningWebhookEvent | Interface | Normalized webhook event |
ICreateSigningRequest | Interface | Signing request creation input |
ICreateSigningSessionRequest | Interface | Session creation input |
SigningRequestStatuses | Constant | Signing request status values |
SignerStatuses | Constant | Signer status values |
SignerRoles | Constant | Signer role values |
SigningFieldTypes | Constant | Signing field type values |
WebhookEventTypes | Constant | Webhook event type values |
EsigningError | Error | Base error class |
SigningProviderError | Error | Provider-level error |
SigningRequestNotFoundError | Error | Not found error |
WebhookVerificationError | Error | Webhook verification error |
ESIGNING_PROVIDER | Token | DI token for provider |
ESIGNING_PROVIDER_OPTIONS | Token | DI token for provider options |
ESIGNING_API_KEY | Config Key | Generic API key config |
ESIGNING_WEBHOOK_SECRET | Config Key | Generic webhook secret config |
ESIGNING_API_BASE_URL | Config Key | Generic base URL config |