Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# production
/build

**/dist/*
# misc
.DS_Store
.env.local
Expand Down
6 changes: 6 additions & 0 deletions packages/v1-ready/docusign/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DOCUSIGN_CLIENT_ID=
DOCUSIGN_CLIENT_SECRET=
DOCUSIGN_ENVIRONMENT=dev
REDIRECT_URI=http://localhost:3000

DOCUSIGN_SCOPE=
42 changes: 42 additions & 0 deletions packages/v1-ready/docusign/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# DocuSign API Module

This is the API Module for DocuSign that allows the [Frigg Framework](https://friggframework.org) to interact with the DocuSign eSignature REST API.

## Features

Currently implemented:
* List Envelopes
* Get Envelope Details
* Create Envelope
* Void Envelope
* List Templates
* Get Template Details
* Retrieve User Info (for account discovery)

## Setup

1. Install dependencies: `npm install`
2. Configure environment variables by copying `.env.example` to `.env` and filling in the required values (Client ID, Client Secret, Environment).

## Usage

```typescript
import { Api, definition } from '@friggframework/api-module-docusign';

// Configuration typically loaded from environment variables
const config = {
client_id: process.env.DOCUSIGN_CLIENT_ID,
client_secret: process.env.DOCUSIGN_CLIENT_SECRET,
// ... other credentials like access/refresh tokens if available
// ... account_id might be set here or retrieved later
};

const api = new Api(config);

// Example: List sent envelopes
api.listEnvelopes({ status: 'sent' })
.then(envelopes => console.log(envelopes))
.catch(error => console.error(error));
```

Read more on the [Frigg documentation site](https://docs.friggframework.org/).
308 changes: 308 additions & 0 deletions packages/v1-ready/docusign/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
import { OAuth2Requester } from '@friggframework/core';

interface DocuSignConstructorParams {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docusign (lower case the S)

client_id: string;
client_secret: string;
redirect_uri: string;
scope: string;
environment: 'dev' | 'prod'; // Added environment
state?: string;
access_token?: string;
refresh_token?: string;
base_url?: string; // User override OR stored base_uri from entity
account_id?: string;
}

interface EnvelopeDefinition {
// Define structure based on https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/
emailSubject: string;
documents: any[]; // Replace 'any' with a proper Document type
recipients: any; // Replace 'any' with a proper Recipients type (signers, carbonCopies etc.)
status: 'sent' | 'created'; // Typically 'sent' to send immediately, 'created' for draft
[key: string]: any; // Allow other properties
}

interface VoidEnvelopeRequest {
status: 'voided';
voidedReason: string;
}

interface TokenResponse {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
[key: string]: any;
}

interface ListTemplatesQueryParams {
count?: number;
start_position?: number;
from_date?: string; // ISO 8601 format
to_date?: string; // ISO 8601 format
search_text?: string;
order?: 'asc' | 'desc';
order_by?: 'name' | 'modified' | 'used';
folder_ids?: string; // Comma-separated list of folder IDs
include?: string; // Comma-separated list (e.g., 'documents,recipients,tabs')
user_filter?: 'all' | 'owned_by_me' | 'shared_with_me';
shared_by_me?: string; // 'true' or 'false'
used_from_date?: string; // ISO 8601 format
used_to_date?: string; // ISO 8601 format
}

interface GetTemplateQueryParams {
include?: string; // Comma-separated list (e.g., 'documents,recipients,tabs')
}

export class Api extends OAuth2Requester {
protected accountId?: string;
protected baseUriHost: string; // Renamed from baseUrl - stores the host part
protected environment: 'dev' | 'prod';
public authorizationUri: string;
public tokenUri: string;
protected authHost: string;

declare public client_id: string;
declare public client_secret: string;
declare public redirect_uri: string;

constructor(params: DocuSignConstructorParams) {
super(params);
this.environment = params.environment || 'dev';

this.authHost = this.environment === 'prod'
? 'https://account.docusign.com'
: 'https://account-d.docusign.com';


let host = params.base_url;
if (!host) {
host = this.environment === 'prod'
? 'https://docusign.net'
: 'https://demo.docusign.net';
}
this.baseUriHost = host;
this.accountId = params.account_id;

this.authorizationUri = `${this.authHost}/oauth/auth`;
this.tokenUri = `${this.authHost}/oauth/token`;
}

setAccountId(accountId: string) {
this.accountId = accountId;
}

private _getAccountApiBaseUrl(): string {
if (!this.accountId) {
throw new Error('DocuSign Account ID is required but not set.');
}
if (!this.baseUriHost) {
throw new Error('DocuSign Base URI Host is required but not set.');
}

return `${this.baseUriHost}/restapi/v2.1/accounts/${this.accountId}`;
}

getAuthorizationUri(): string {
const baseUri = `${this.authHost}/oauth/auth`;
const params = new URLSearchParams();

params.append('response_type', 'code');
params.append('client_id', this.client_id);
params.append('redirect_uri', this.redirect_uri);
params.append('scope', this.scope);
if (this.state) {
params.append('state', this.state);
}

// Note: Does not include PKCE parameters (code_challenge)
// Consider adding if needed for enhanced security, requires generating code_verifier.

return `${baseUri}?${params.toString()}`;
}

addJsonHeaders(options: any) {
const jsonHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
options.headers = {
...jsonHeaders,
...options.headers,
};
}

async _get(options: any) {
this.addJsonHeaders(options);
return super._get(options);
}

async _post(options: any, stringify?: boolean) {
this.addJsonHeaders(options);
return super._post(options, stringify);
}

async _put(options: any, stringify?: boolean) {
this.addJsonHeaders(options);
return super._put(options, stringify);
}

async _patch(options: any, stringify?: boolean) {
this.addJsonHeaders(options);
return super._patch(options, stringify);
}

async _delete(options: any) {
this.addJsonHeaders(options);
return super._delete(options);
}

// Method to handle the OAuth token exchange (authorization code flow)
async getTokenFromCode(code: string): Promise<TokenResponse> {
const url = this.tokenUri;

// Ensure 'this' is used for base class protected members
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirect_uri, // Explicit this
}).toString();

const clientCredentials = Buffer.from(
`${this.client_id}:${this.client_secret}` // Explicit this
).toString('base64');

const options = {
url: url,
headers: {
Authorization: `Basic ${clientCredentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body,
};

try {
const response = await fetch(url, {
method: 'POST',
headers: options.headers,
body: options.body,
});

const data = await response.json();

if (!response.ok) {
const errorPayload = data || { message: response.statusText };
throw new Error(
`Token exchange failed with status ${response.status}: ${JSON.stringify(errorPayload)}`
);
}

this.setTokens(data);

return data as TokenResponse;
} catch (error: any) {
console.error('Error during token exchange:', error);
throw new Error(`Token exchange request failed: ${error.message}`);
}
}

/**
* Retrieves the list of envelopes for the account.
* DocuSign API: GET /envelopes
* @param query Optional query parameters
*/
async listEnvelopes(query?: Record<string, any>) {
const baseUrl = this._getAccountApiBaseUrl();
const options = {
url: `${baseUrl}/envelopes`, // Append specific endpoint
query: query || {},
};
return this._get(options);
}

/**
* Retrieves the details of a specific envelope.
* DocuSign API: GET /envelopes/{envelopeId}
* @param envelopeId The ID of the envelope to retrieve.
* @param query Optional query parameters
*/
async getEnvelope(envelopeId: string, query?: Record<string, any>) {
const baseUrl = this._getAccountApiBaseUrl();
const options = {
url: `${baseUrl}/envelopes/${envelopeId}`, // Append specific endpoint
query: query || {},
};
return this._get(options);
}

/**
* Creates a new envelope.
* DocuSign API: POST /envelopes
* @param definition The envelope definition
*/
async createEnvelope(definition: EnvelopeDefinition) {
const baseUrl = this._getAccountApiBaseUrl();
const options = {
url: `${baseUrl}/envelopes`, // Append specific endpoint
body: definition,
};
return this._post(options);
}

/**
* Voids a sent envelope that is still in process.
* DocuSign API: PUT /envelopes/{envelopeId}
* @param envelopeId The ID of the envelope to void.
* @param reason The reason for voiding the envelope.
*/
async voidEnvelope(envelopeId: string, reason: string) {
const baseUrl = this._getAccountApiBaseUrl();
const body: VoidEnvelopeRequest = {
status: 'voided',
voidedReason: reason,
};
const options = {
url: `${baseUrl}/envelopes/${envelopeId}`, // Append specific endpoint
body: body,
};
return this._put(options);
}

/**
* Retrieves the list of templates for the account.
* DocuSign API: GET /templates
* @param query Optional query parameters
*/
async listTemplates(query?: ListTemplatesQueryParams) {
const baseUrl = this._getAccountApiBaseUrl();
const options = {
url: `${baseUrl}/templates`, // Append specific endpoint
query: query || {},
};
return this._get(options);
}

/**
* Retrieves the details of a specific template.
* DocuSign API: GET /templates/{templateId}
* @param templateId The ID of the template to retrieve.
* @param query Optional query parameters
*/
async getTemplate(templateId: string, query?: GetTemplateQueryParams) {
const baseUrl = this._getAccountApiBaseUrl();
const options = {
url: `${baseUrl}/templates/${templateId}`, // Append specific endpoint
query: query || {},
};
return this._get(options);
}

async getUserInfo() {
const options = {
url: `${this.authHost}/oauth/userinfo`,
};
this.addJsonHeaders(options);
return super._get(options);
}
}
13 changes: 13 additions & 0 deletions packages/v1-ready/docusign/defaultConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "docusign",
"label": "DocuSign",
"productUrl": "https://www.docusign.com/",
"apiDocs": "https://developers.docusign.com/docs/esign-rest-api/",
"logoUrl": "https://static.wikia.nocookie.net/logopedia/images/a/ac/DocuSign_2024_S.svg/revision/latest/scale-to-width-down/250?cb=20240417143304",
"categories": [
"eSignature",
"Document Management",
"Workflow Automation"
],
"description": "DocuSign helps organizations connect and automate how they prepare, sign, act on, and manage agreements."
}
Loading
Loading