feat(sdk): add typescript types actionable errors and mock server tests

This commit is contained in:
Claudecio Martins
2026-10-20 16:35:29 -03:00
parent e9f90b42cb
commit 957f9c9abd
8 changed files with 879 additions and 19 deletions
+114
View File
@@ -0,0 +1,114 @@
/**
* Definições TypeScript para os erros estruturados do protocolo CCSP/1 (@cybercore/signer-sdk).
*/
export type CcspErrorCode =
| 'AGENT_NOT_RUNNING'
| 'COMMUNICATION_ERROR'
| 'APPLICATION_NOT_AUTHORIZED'
| 'USER_CANCELLED'
| 'CERTIFICATE_NOT_FOUND'
| 'CERTIFICATE_EXPIRED'
| 'CERTIFICATE_INVALID'
| 'PRIVATE_KEY_NOT_AVAILABLE'
| 'PIN_REQUIRED'
| 'PIN_INCORRECT'
| 'TOKEN_NOT_FOUND'
| 'TOKEN_REMOVED'
| 'PKCS11_ERROR'
| 'REQUEST_EXPIRED'
| 'REPLAY_DETECTED'
| 'INVALID_ORIGIN'
| 'UNSUPPORTED_ALGORITHM'
| 'PROVIDER_NOT_AVAILABLE'
| 'SIGNATURE_FAILED'
| 'TIMEOUT_ERROR'
| 'BAD_REQUEST'
| 'SIGN_ERROR'
| 'SIGN_PDF_ERROR'
| 'SIGN_XML_ERROR'
| 'AUTH_ERROR'
| 'CONNECTION_FAILED'
| string;
export class CyberCoreError extends Error {
code: CcspErrorCode;
originalError?: unknown;
constructor(message: string, code?: CcspErrorCode, originalError?: unknown);
}
export interface AgentNotRunningOptions {
host?: string;
port?: number;
}
export class AgentNotRunningError extends CyberCoreError {
host: string;
port: number;
actionableGuidance: string;
constructor(message?: string, options?: AgentNotRunningOptions);
}
export class ApplicationNotAuthorizedError extends CyberCoreError {
constructor(message?: string);
}
export class UserCancelledError extends CyberCoreError {
constructor(message?: string);
}
export class CertificateNotFoundError extends CyberCoreError {
constructor(message?: string);
}
export class CertificateExpiredError extends CyberCoreError {
constructor(message?: string);
}
export class CertificateInvalidError extends CyberCoreError {
constructor(message?: string);
}
export class PrivateKeyNotAvailableError extends CyberCoreError {
constructor(message?: string);
}
export class PinRequiredError extends CyberCoreError {
constructor(message?: string);
}
export class PinIncorrectError extends CyberCoreError {
constructor(message?: string);
}
export class TokenNotFoundError extends CyberCoreError {
constructor(message?: string);
}
export class RequestExpiredError extends CyberCoreError {
constructor(message?: string);
}
export class ReplayDetectedError extends CyberCoreError {
constructor(message?: string);
}
export class InvalidOriginError extends CyberCoreError {
constructor(message?: string);
}
export class UnsupportedAlgorithmError extends CyberCoreError {
constructor(message?: string);
}
export class ProviderNotAvailableError extends CyberCoreError {
constructor(message?: string);
}
export class SignatureFailedError extends CyberCoreError {
constructor(message?: string);
}
export class TimeoutError extends CyberCoreError {
constructor(message?: string);
}
+72 -6
View File
@@ -12,9 +12,19 @@ export class CyberCoreError extends Error {
}
export class AgentNotRunningError extends CyberCoreError {
constructor(message = 'O agente desktop CyberCore Signer não está em execução ou não foi detectado em 127.0.0.1:21325.') {
constructor(
message = 'O agente desktop CyberCore Signer não está em execução ou não foi detectado em 127.0.0.1:21325. Certifique-se de que o aplicativo está instalado e aberto na bandeja do sistema (system tray).',
options = {}
) {
super(message, 'AGENT_NOT_RUNNING');
this.name = 'AgentNotRunningError';
this.host = options.host || '127.0.0.1';
this.port = options.port || 21325;
this.actionableGuidance = [
'1. Verifique se o ícone do CyberCore Signer está ativo na barra de tarefas / bandeja do sistema.',
'2. Se o aplicativo ainda não estiver instalado, baixe e execute o instalador oficial do CyberCore Signer.',
'3. Certifique-se de que a porta local 21325 não está bloqueada por firewall ou softwares antivírus.'
].join('\n');
}
}
@@ -26,21 +36,42 @@ export class ApplicationNotAuthorizedError extends CyberCoreError {
}
export class UserCancelledError extends CyberCoreError {
constructor(message = 'A operação de assinatura foi cancelada pelo usuário.') {
constructor(message = 'A operação de assinatura foi cancelada pelo usuário no diálogo nativo.') {
super(message, 'USER_CANCELLED');
this.name = 'UserCancelledError';
}
}
export class CertificateNotFoundError extends CyberCoreError {
constructor(message = 'Certificado digital não encontrado ou não selecionado.') {
constructor(message = 'Certificado digital não encontrado ou não selecionado no agente.') {
super(message, 'CERTIFICATE_NOT_FOUND');
this.name = 'CertificateNotFoundError';
}
}
export class CertificateExpiredError extends CyberCoreError {
constructor(message = 'O certificado digital selecionado expirou e não pode ser utilizado para assinatura.') {
super(message, 'CERTIFICATE_EXPIRED');
this.name = 'CertificateExpiredError';
}
}
export class CertificateInvalidError extends CyberCoreError {
constructor(message = 'O certificado digital é inválido, foi revogado ou possui estrutura X.509 corrompida.') {
super(message, 'CERTIFICATE_INVALID');
this.name = 'CertificateInvalidError';
}
}
export class PrivateKeyNotAvailableError extends CyberCoreError {
constructor(message = 'A chave privada do certificado não está acessível no computador ou o dispositivo físico está ausente.') {
super(message, 'PRIVATE_KEY_NOT_AVAILABLE');
this.name = 'PrivateKeyNotAvailableError';
}
}
export class PinRequiredError extends CyberCoreError {
constructor(message = 'PIN do token ou smart card A3 é obrigatório.') {
constructor(message = 'O PIN do token USB ou smart card A3 é obrigatório para autorizar a chave privada.') {
super(message, 'PIN_REQUIRED');
this.name = 'PinRequiredError';
}
@@ -54,14 +85,14 @@ export class PinIncorrectError extends CyberCoreError {
}
export class TokenNotFoundError extends CyberCoreError {
constructor(message = 'Token USB ou smart card não foi detectado no computador.') {
constructor(message = 'Token USB ou smart card não foi detectado no computador ou foi removido.') {
super(message, 'TOKEN_NOT_FOUND');
this.name = 'TokenNotFoundError';
}
}
export class RequestExpiredError extends CyberCoreError {
constructor(message = 'A solicitação de assinatura expirou.') {
constructor(message = 'A solicitação de assinatura expirou antes de ser processada.') {
super(message, 'REQUEST_EXPIRED');
this.name = 'RequestExpiredError';
}
@@ -73,3 +104,38 @@ export class ReplayDetectedError extends CyberCoreError {
this.name = 'ReplayDetectedError';
}
}
export class InvalidOriginError extends CyberCoreError {
constructor(message = 'A origem web solicitante não confere com os domínios homologados para esta aplicação.') {
super(message, 'INVALID_ORIGIN');
this.name = 'InvalidOriginError';
}
}
export class UnsupportedAlgorithmError extends CyberCoreError {
constructor(message = 'O algoritmo criptográfico solicitado não é suportado pelo provedor de certificados.') {
super(message, 'UNSUPPORTED_ALGORITHM');
this.name = 'UnsupportedAlgorithmError';
}
}
export class ProviderNotAvailableError extends CyberCoreError {
constructor(message = 'O provedor criptográfico necessário não está disponível no sistema operacional.') {
super(message, 'PROVIDER_NOT_AVAILABLE');
this.name = 'ProviderNotAvailableError';
}
}
export class SignatureFailedError extends CyberCoreError {
constructor(message = 'Falha durante o cálculo da assinatura digital.') {
super(message, 'SIGNATURE_FAILED');
this.name = 'SignatureFailedError';
}
}
export class TimeoutError extends CyberCoreError {
constructor(message = 'Tempo limite esgotado na comunicação com o agente local.') {
super(message, 'TIMEOUT_ERROR');
this.name = 'TimeoutError';
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './signer-sdk.js';
export { default } from './signer-sdk.js';
+246
View File
@@ -0,0 +1,246 @@
/**
* CyberCore Signer Web SDK (@cybercore/signer-sdk)
* Protocolo: CCSP/1 (CyberCore Signing Protocol v1)
*/
export * from './errors.js';
export interface CyberCoreSignerOptions {
/** URL base completa do agente local (prevalece sobre host e port). Ex: 'http://127.0.0.1:21325' */
baseUrl?: string;
/** Host do agente local (padrão: '127.0.0.1') */
host?: string;
/** Porta HTTP do agente local (padrão: 21325) */
port?: number;
/** Timeout padrão para requisições em milissegundos (padrão: 30000) */
timeoutMs?: number;
/** UUID persistido da aplicação web */
applicationId?: string;
/** Nome legível da aplicação exibido na confirmação de assinatura */
applicationName?: string;
}
export interface StatusResponse {
version: string;
status: string;
agent_version?: string;
os?: string;
ready?: boolean;
[key: string]: unknown;
}
export interface VersionInfo {
protocolVersion: string;
agentVersion: string;
os: string;
}
export interface CapabilitiesResponse {
protocolVersion: string;
supportedAlgorithms: string[];
supportedHashAlgorithms: string[];
certificateTypes: ('A1' | 'A3')[];
hardwareSupport: string[];
padesSupport: boolean;
[key: string]: unknown;
}
export interface CertificateDto {
id: string;
alias?: string;
subject_name: string;
cpf_cnpj?: string;
issuer_name?: string;
serial_number?: string;
valid_from?: string;
valid_to?: string;
certificate_type: 'A1' | 'A3';
has_private_key: boolean;
key_algorithm?: string;
provider_type?: string;
[key: string]: unknown;
}
export interface SignParams {
certificateId?: string;
certificateHint?: string;
digest?: string;
data?: string | Uint8Array | ArrayBuffer;
algorithm?: 'SHA256withRSA' | 'SHA1withRSA' | string;
operation?: 'sign' | string;
documentName?: string;
fileName?: string;
documentType?: string;
fileType?: string;
purpose?: string;
expiresInSeconds?: number;
}
export interface SignResult {
requestId: string;
status: 'signed' | 'authenticated';
signature: string;
certificate: string;
certificateChain: string[];
algorithm: string;
digest: string;
subjectName: string | null;
subjectCpfCnpj: string | null;
documentName: string | null;
documentType: string | null;
}
export interface SignPdfDocumentParams {
pdf: string | File | Blob | ArrayBuffer | Uint8Array;
certificateId?: string;
certificateHint?: string;
documentName?: string;
fileName?: string;
page?: number;
pageChoice?: 'LAST_PAGE' | 'FIRST_PAGE' | null;
position?: 'BOTTOM_RIGHT' | 'BOTTOM_LEFT' | 'TOP_RIGHT' | 'TOP_LEFT' | 'CUSTOM' | string;
x?: number;
y?: number;
scale?: number;
visibleSeal?: boolean;
sealThemeId?: string;
sealCustom?: Record<string, unknown>;
reason?: string;
location?: string;
cpfDisplayMode?: 'FULL' | 'MASKED' | 'NONE';
includeDateTime?: boolean;
customExtraInfo?: string;
expiresInSeconds?: number;
}
export interface SignPdfDocumentResult {
requestId: string;
status: 'signed';
signedPdf: string;
signed_pdf: string;
sha256?: string;
signature?: string;
certificate: string;
certificateChain: string[];
algorithm: string;
subjectName: string | null;
subjectCpfCnpj: string | null;
documentName: string;
getSignedPdfBlob(): Blob;
getSignedPdfBytes(): Uint8Array;
download(filename?: string): void;
}
export interface SignXmlDocumentParams {
xml: string | File | Blob | ArrayBuffer;
certificateId?: string;
certificateHint?: string;
profile?: 'XADES_BES' | 'XMLDSIG' | string;
targetElementTag?: string;
targetElementId?: string;
documentName?: string;
fileName?: string;
algorithm?: string;
expiresInSeconds?: number;
}
export interface SignXmlDocumentResult {
requestId: string;
status: 'signed';
signedXml: string;
sha256?: string;
signature?: string;
certificate: string;
certificateChain: string[];
algorithm: string;
subjectName: string | null;
subjectCpfCnpj: string | null;
documentName: string;
getSignedXmlBlob(): Blob;
getSignedXmlBytes(): Uint8Array;
download(filename?: string): void;
}
export interface AuthenticateParams {
certificateId?: string;
certificateHint?: string;
challenge?: string;
algorithm?: string;
expiresInSeconds?: number;
}
export interface AuthenticateResult {
requestId: string;
status: 'authenticated';
signature: string;
certificate: string;
certificateChain: string[];
algorithm: string;
challenge: string;
subjectName: string;
subjectCpfCnpj: string;
}
export interface VerifyParams {
signature: string;
digest: string;
certificate: string;
}
export interface SealThemesResponse {
version: string;
status: string;
default_seal_id?: string;
seals: Array<{ id: string; name: string; [key: string]: unknown }>;
}
export interface SubjectInfo {
name: string;
cpfCnpj: string;
}
export class CyberCoreSigner {
baseUrl: string;
host: string;
port: number;
timeoutMs: number;
applicationId: string;
applicationName: string;
connected: boolean;
constructor(options?: CyberCoreSignerOptions);
isInstalled(timeoutMs?: number): Promise<boolean>;
getStatus(timeoutMs?: number): Promise<StatusResponse>;
getVersion(): Promise<VersionInfo>;
getCapabilities(): Promise<CapabilitiesResponse>;
connect(): Promise<boolean>;
getCertificates(): Promise<CertificateDto[]>;
sign(params: SignParams): Promise<SignResult>;
signFile(file: File | Blob | ArrayBuffer | Uint8Array, options?: Partial<SignParams>): Promise<SignResult>;
signPdf(pdfFile: File | Blob | ArrayBuffer | Uint8Array, options?: Partial<SignParams>): Promise<SignResult>;
signXml(xmlContent: string | File | Blob | ArrayBuffer, options?: Partial<SignParams>): Promise<SignResult>;
getSealThemes(): Promise<SealThemesResponse>;
signPdfDocument(params: SignPdfDocumentParams): Promise<SignPdfDocumentResult>;
signXmlDocument(params: SignXmlDocumentParams): Promise<SignXmlDocumentResult>;
authenticate(options?: AuthenticateParams): Promise<AuthenticateResult>;
login(options?: AuthenticateParams): Promise<AuthenticateResult>;
extractSubjectInfo(certificateBase64: string): SubjectInfo;
verify(params: VerifyParams): Promise<boolean>;
_fetch(endpoint: string, options?: Record<string, unknown>): Promise<any>;
_calculateSha256(data: string | ArrayBuffer | ArrayBufferView): Promise<string>;
_throwMappedError(code: string, message?: string): never;
}
export class PdfStampPicker {
constructor(options?: unknown);
open(options?: unknown): Promise<unknown>;
}
export class PdfSealDesigner {
constructor(options?: unknown);
open(options?: unknown): Promise<unknown>;
}
export default CyberCoreSigner;
+75 -11
View File
@@ -9,29 +9,66 @@ import {
ApplicationNotAuthorizedError,
UserCancelledError,
CertificateNotFoundError,
CertificateExpiredError,
CertificateInvalidError,
PrivateKeyNotAvailableError,
PinRequiredError,
PinIncorrectError,
TokenNotFoundError,
RequestExpiredError,
ReplayDetectedError
ReplayDetectedError,
InvalidOriginError,
UnsupportedAlgorithmError,
ProviderNotAvailableError,
SignatureFailedError,
TimeoutError
} from './errors.js';
import { PdfStampPicker } from './pdf-stamp-picker.js';
import { PdfSealDesigner } from './pdf-seal-designer.js';
export { PdfStampPicker, PdfSealDesigner };
export {
PdfStampPicker,
PdfSealDesigner,
CyberCoreError,
AgentNotRunningError,
ApplicationNotAuthorizedError,
UserCancelledError,
CertificateNotFoundError,
CertificateExpiredError,
CertificateInvalidError,
PrivateKeyNotAvailableError,
PinRequiredError,
PinIncorrectError,
TokenNotFoundError,
RequestExpiredError,
ReplayDetectedError,
InvalidOriginError,
UnsupportedAlgorithmError,
ProviderNotAvailableError,
SignatureFailedError,
TimeoutError
};
export class CyberCoreSigner {
/**
* Inicializa o cliente do SDK.
*
* @param {Object} options Configurações do SDK.
* @param {string} [options.baseUrl='http://127.0.0.1:21325'] URL base do agente local.
* @param {string} [options.applicationId] UUID único da aplicação web (gerado se não fornecido).
* @param {Object} [options={}] Configurações do SDK.
* @param {string} [options.baseUrl] URL base completa do agente local (prevalece sobre host e port).
* @param {string} [options.host='127.0.0.1'] Host do agente local.
* @param {number} [options.port=21325] Porta HTTP do agente local.
* @param {number} [options.timeoutMs=30000] Timeout padrão das operações em milissegundos.
* @param {string} [options.applicationId] UUID único da aplicação web (gerado e persistido se omitido).
* @param {string} [options.applicationName] Nome legível da aplicação apresentado ao usuário.
*/
constructor(options = {}) {
this.baseUrl = (options.baseUrl || 'http://127.0.0.1:21325').replace(/\/+$/, '');
const host = options.host || '127.0.0.1';
const port = options.port !== undefined ? options.port : 21325;
this.host = host;
this.port = port;
this.baseUrl = (options.baseUrl || `http://${host}:${port}`).replace(/\/+$/, '');
this.timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : 30000;
this.applicationId = options.applicationId || this._getOrCreateApplicationId();
this.applicationName = options.applicationName || (typeof document !== 'undefined' ? document.title : 'Aplicação Web');
this.connected = false;
@@ -63,8 +100,15 @@ export class CyberCoreSigner {
const resp = await this._fetch('/ccsp/v1/status', { method: 'GET', timeoutMs });
return resp;
} catch (err) {
if (err.name === 'AbortError' || err.message?.includes('Failed to fetch') || err.message?.includes('NetworkError')) {
throw new AgentNotRunningError();
if (
err.name === 'AbortError' ||
err instanceof TimeoutError ||
err instanceof AgentNotRunningError ||
err.message?.includes('Failed to fetch') ||
err.message?.includes('NetworkError') ||
err.code === 'ECONNREFUSED'
) {
throw new AgentNotRunningError(undefined, { host: this.host, port: this.port });
}
throw err;
}
@@ -702,7 +746,7 @@ export class CyberCoreSigner {
*/
async _fetch(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const timeout = options.timeoutMs || 15000;
const timeout = options.timeoutMs !== undefined ? options.timeoutMs : (this.timeoutMs || 30000);
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timeoutId = controller ? setTimeout(() => controller.abort(), timeout) : null;
@@ -738,12 +782,15 @@ export class CyberCoreSigner {
return data;
} catch (err) {
if (err.name === 'AbortError') {
throw new CyberCoreError(`Tempo limite esgotado (${timeout}ms) na comunicação com o agente.`, 'TIMEOUT_ERROR');
throw new TimeoutError(`Tempo limite esgotado (${timeout}ms) na comunicação com o agente local.`);
}
if (err instanceof CyberCoreError) {
throw err;
}
throw new AgentNotRunningError(`Falha ao conectar com o CyberCore Signer em ${this.baseUrl}: ${err.message}`);
throw new AgentNotRunningError(
`Falha ao conectar com o CyberCore Signer em ${this.baseUrl}: ${err.message}`,
{ host: this.host, port: this.port }
);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
@@ -829,6 +876,12 @@ export class CyberCoreSigner {
throw new UserCancelledError(message);
case 'CERTIFICATE_NOT_FOUND':
throw new CertificateNotFoundError(message);
case 'CERTIFICATE_EXPIRED':
throw new CertificateExpiredError(message);
case 'CERTIFICATE_INVALID':
throw new CertificateInvalidError(message);
case 'PRIVATE_KEY_NOT_AVAILABLE':
throw new PrivateKeyNotAvailableError(message);
case 'APPLICATION_NOT_AUTHORIZED':
throw new ApplicationNotAuthorizedError(message);
case 'PIN_REQUIRED':
@@ -836,11 +889,22 @@ export class CyberCoreSigner {
case 'PIN_INCORRECT':
throw new PinIncorrectError(message);
case 'TOKEN_NOT_FOUND':
case 'TOKEN_REMOVED':
throw new TokenNotFoundError(message);
case 'REQUEST_EXPIRED':
throw new RequestExpiredError(message);
case 'REPLAY_DETECTED':
throw new ReplayDetectedError(message);
case 'INVALID_ORIGIN':
throw new InvalidOriginError(message);
case 'UNSUPPORTED_ALGORITHM':
throw new UnsupportedAlgorithmError(message);
case 'PROVIDER_NOT_AVAILABLE':
throw new ProviderNotAvailableError(message);
case 'SIGNATURE_FAILED':
throw new SignatureFailedError(message);
case 'TIMEOUT_ERROR':
throw new TimeoutError(message);
default:
throw new CyberCoreError(message || `Erro do protocolo: ${code}`, code);
}