Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8c4da68
feat(docusign): implement eSignature client, envelope/template endpoi…
likithdt Aug 26, 2026
b6885a1
test(docusign): verify plugin registration and client in demo server
likithdt Aug 26, 2026
fc3556f
fix(docusign): remove polynomial regex to resolve codeql redos warning
likithdt Aug 26, 2026
e7543f3
style(docusign): format and clean up package with biome
likithdt Aug 26, 2026
7bdeb16
chore: revert demo/testing to keep PR strictly scoped to plugin
likithdt Aug 26, 2026
96444b2
chore: sync pnpm-lock.yaml after reverting demo/testing
likithdt Aug 26, 2026
ef2809a
chore: trigger PR gate check
likithdt Aug 26, 2026
6ca4695
chore: trigger PR gate check
likithdt Aug 26, 2026
0e70ada
fix(docusign): export endpoint schemas and satisfy RequiredPluginEndp…
likithdt Aug 26, 2026
c2fc142
fix(docusign): export DocusignSchema from schema folder and pass tests
likithdt Aug 26, 2026
7ec3fea
fix(docusign): update endpoint risk levels to read and write
likithdt Aug 26, 2026
ee4fdde
style(docusign): apply biome formatting
likithdt Aug 27, 2026
be59088
fix(docusign): structure webhook export with match and handler
likithdt Aug 27, 2026
36c0a69
fix(docusign): implement rateLimit and auth errorHandlers on plugin
likithdt Aug 27, 2026
7cbb3f3
fix(docusign): update webhook handler signature to (context, request)
likithdt Aug 27, 2026
f9e85a1
fix(docusign): resolve context client binding and schemas
likithdt Aug 27, 2026
592c0b7
fix(docusign): update endpoint context typing
likithdt Aug 27, 2026
88f600f
fix(docusign): update endpoint context typing to support client conte…
likithdt Aug 27, 2026
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
3 changes: 3 additions & 0 deletions packages/corsair/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const BaseProviders = [
'digitalocean',
'discord',
'dockerhub',
'docusign',
'dodopayments',
'doppler',
'dropbox',
Expand Down Expand Up @@ -296,6 +297,7 @@ export const ProviderDisplayNames = {
digitalocean: 'DigitalOcean',
discord: 'Discord',
dockerhub: 'Docker Hub',
docusign: 'Docusign',
dodopayments: 'Dodo Payments',
doppler: 'Doppler',
dropbox: 'Dropbox',
Expand Down Expand Up @@ -498,6 +500,7 @@ export type AllProviders =
| 'digitalocean'
| 'discord'
| 'dockerhub'
| 'docusign'
| 'dodopayments'
| 'doppler'
| 'dropbox'
Expand Down
38 changes: 38 additions & 0 deletions packages/docusign/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface DocusignAuthOptions {
accessToken: string;
accountId: string;
baseUri?: string;
}

export class DocusignClient {
public baseUri: string;
public accountId: string;
private token: string;

constructor(options: DocusignAuthOptions) {
this.accountId = options.accountId;
this.token = options.accessToken;
const root = options.baseUri || 'https://demo.docusign.net/restapi';
this.baseUri = `${root.replace(/\/+$/, '')}/v2.1/accounts/${this.accountId}`;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
likithdt marked this conversation as resolved.
Outdated
}

async request<T = any>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUri}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
},
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`DocuSign API Error (${response.status}): ${errorText}`);
}

return response.json() as Promise<T>;
}
}
68 changes: 68 additions & 0 deletions packages/docusign/endpoints/envelopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { DocusignClient } from '../client';

export interface CreateEnvelopeParams {
templateId?: string;
emailSubject: string;
status: 'sent' | 'created';
templateRoles?: Array<{
email: string;
name: string;
roleName: string;
}>;
documents?: Array<{
documentId: string;
name: string;
fileExtension?: string;
documentBase64?: string;
}>;
recipients?: {
signers?: Array<{
email: string;
name: string;
recipientId: string;
routingOrder?: string;
}>;
};
}

export const createEnvelope = async (client: DocusignClient, params: CreateEnvelopeParams) => {
return client.request('/envelopes', {
method: 'POST',
body: JSON.stringify(params),
});
};

export const getEnvelope = async (client: DocusignClient, { envelopeId }: { envelopeId: string }) => {
return client.request(`/envelopes/${envelopeId}`);
};

export const sendEnvelope = async (client: DocusignClient, { envelopeId }: { envelopeId: string }) => {
return client.request(`/envelopes/${envelopeId}`, {
method: 'PUT',
body: JSON.stringify({ status: 'sent' }),
});
};

export const createRecipientViewUrl = async (
client: DocusignClient,
{
envelopeId,
...params
}: {
envelopeId: string;
userName: string;
email: string;
returnUrl: string;
authenticationMethod?: string;
recipientId?: string;
}
) => {
return client.request(`/envelopes/${envelopeId}/views/recipient`, {
method: 'POST',
body: JSON.stringify({
authenticationMethod: 'none',
recipientId: '1',
...params,
}),
});
};
34 changes: 34 additions & 0 deletions packages/docusign/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { DocusignClient } from '../client';
import type { CreateEnvelopeParams, GetEnvelopeParams, ListTemplatesParams } from './types';

export const createEnvelope = async (client: DocusignClient, params: CreateEnvelopeParams) => {
return client.request('/envelopes', {
method: 'POST',
body: JSON.stringify(params),
});
};

export const getEnvelope = async (client: DocusignClient, params: GetEnvelopeParams) => {
return client.request(`/envelopes/${params.envelopeId}`);
};

export const sendEnvelope = async (client: DocusignClient, params: GetEnvelopeParams) => {
return client.request(`/envelopes/${params.envelopeId}`, {
method: 'PUT',
body: JSON.stringify({ status: 'sent' }),
});
};

export const listTemplates = async (client: DocusignClient, params?: ListTemplatesParams) => {
const query = new URLSearchParams();
if (params?.count) query.append('count', String(params.count));
if (params?.startPosition) query.append('start_position', String(params.startPosition));
const qs = query.toString() ? `?${query.toString()}` : '';
return client.request(`/templates${qs}`);
};

export const getTemplate = async (client: DocusignClient, params: { templateId: string }) => {
return client.request(`/templates/${params.templateId}`);
};

export * from './types';
16 changes: 16 additions & 0 deletions packages/docusign/endpoints/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DocusignClient } from '../client';

export const listTemplates = async (
client: DocusignClient,
params?: { count?: number; startPosition?: number }
) => {
const query = new URLSearchParams();
if (params?.count) query.append('count', String(params.count));
if (params?.startPosition) query.append('start_position', String(params.startPosition));
const qs = query.toString() ? `?${query.toString()}` : '';
return client.request(`/templates${qs}`);
};

export const getTemplate = async (client: DocusignClient, { templateId }: { templateId: string }) => {
return client.request(`/templates/${templateId}`);
};
19 changes: 19 additions & 0 deletions packages/docusign/endpoints/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface CreateEnvelopeParams {
templateId?: string;
emailSubject: string;
status: 'sent' | 'created';
templateRoles?: Array<{
email: string;
name: string;
roleName: string;
}>;
}

export interface GetEnvelopeParams {
envelopeId: string;
}

export interface ListTemplatesParams {
count?: number;
startPosition?: number;
}
31 changes: 31 additions & 0 deletions packages/docusign/error-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiError } from 'corsair/http';
import type { CorsairErrorHandler } from 'corsair/core';

export const errorHandlers = {
RATE_LIMIT_ERROR: {
match: (error: Error) => {
if (error instanceof ApiError && error.status === 429) return true;
const msg = error.message.toLowerCase();
return msg.includes('rate_limited') || msg.includes('429');
},
handler: async (error: Error) => {
let retryAfterMs: number | undefined;
if (error instanceof ApiError && error.retryAfter !== undefined) {
retryAfterMs = error.retryAfter;
}
return { maxRetries: 5, headersRetryAfterMs: retryAfterMs };
},
},
AUTH_ERROR: {
match: (error: Error) => {
if (error instanceof ApiError && error.status === 401) return true;
const msg = error.message.toLowerCase();
return msg.includes('unauthorized') || msg.includes('invalid_auth');
},
handler: async () => ({ maxRetries: 0 }),
},
DEFAULT: {
match: () => true,
handler: async () => ({ maxRetries: 0 }),
},
} satisfies CorsairErrorHandler;
56 changes: 56 additions & 0 deletions packages/docusign/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { DocusignClient, type DocusignAuthOptions } from './client';
import * as endpoints from './endpoints';
import * as schema from './schema';
import * as webhooks from './webhooks';

export * from './client';
export * from './endpoints';
export * from './schema';
export * from './webhooks';

export const docusignEndpointsNested = {
createEnvelope: endpoints.createEnvelope,
getEnvelope: endpoints.getEnvelope,
sendEnvelope: endpoints.sendEnvelope,
listTemplates: endpoints.listTemplates,
getTemplate: endpoints.getTemplate,
Comment thread
likithdt marked this conversation as resolved.
};
Comment thread
likithdt marked this conversation as resolved.
Comment thread
likithdt marked this conversation as resolved.

export const docusignWebhooksNested = {
handleWebhook: webhooks.handleWebhook,
};
Comment thread
likithdt marked this conversation as resolved.

export const docusignEndpointMeta = {
createEnvelope: {
description: 'Creates a signing envelope from a pre-existing DocuSign template.',
},
getEnvelope: {
description: 'Gets the status and basic information about an envelope from DocuSign.',
},
sendEnvelope: {
description: 'Sends a draft envelope by updating its status to sent.',
},
listTemplates: {
description: 'Gets the definition of templates in the specified account.',
},
getTemplate: {
description: 'Gets a template definition from the specified account.',
},
};

export const docusignPlugin = {
id: 'docusign',
name: 'DocuSign',
description: 'DocuSign eSignature REST API integration for agreements, envelopes, and templates.',
auth: {
type: 'oauth2' as const,
fields: ['accessToken', 'accountId', 'baseUri'],
},
createClient: (options: DocusignAuthOptions) => new DocusignClient(options),
endpoints: docusignEndpointsNested,
webhooks: docusignWebhooksNested,
endpointMeta: docusignEndpointMeta,
Comment thread
likithdt marked this conversation as resolved.
Outdated
schema,
};

export default docusignPlugin;
55 changes: 55 additions & 0 deletions packages/docusign/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>'],
testMatch: [
'**/*.test.ts',
'**/tests/**/*.test.ts',
'**/plugins/**/*.test.ts',
'**/setup/**/*.test.ts',
],
collectCoverageFrom: [
'**/*.ts',
'!**/*.d.ts',
'!**/node_modules/**',
'!**/dist/**',
'!jest.config.ts',
'!tests/**',
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
transform: {
'^.+\\.yaml$': '<rootDir>/../corsair/jest-yaml-transform.cjs',
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
tsconfig: {
esModuleInterop: true,
allowSyntheticDefaultImports: true,
verbatimModuleSyntax: false,
module: 'ESNext',
moduleResolution: 'Bundler',
},
},
],
'.*\\.js$': [
'ts-jest',
{
useESM: true,
tsconfig: {
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
},
],
},
moduleNameMapper: {
'^corsair/core$': '<rootDir>/../corsair/core.ts',
'^corsair/http$': '<rootDir>/../corsair/http.ts',
'^(\\.\\.?/.*)\\.js$': '$1',
},
transformIgnorePatterns: ['node_modules/(?!.*uuid.*)'],
extensionsToTreatAsEsm: ['.ts'],
testTimeout: 30000,
verbose: true,
};
Loading
Loading