feat: update PDF stamp picker and server CORS settings for enhanced UI rendering

This commit is contained in:
Claudecio Martins
2026-10-11 19:45:30 -03:00
parent b46d8b665f
commit 994a27886e
9 changed files with 440 additions and 123 deletions
@@ -131,12 +131,12 @@ public class CyberCoreSignerApplication {
private static void printBanner() { private static void printBanner() {
System.out.println(""" System.out.println("""
============================================================ ============================================================
____ _ ____ ____ _ ____ \s
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___ / ___| _ _ | |__ ___ _ __ / ___|___ _ __ ___ \s
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\ | | | | | | | '_ \\ / _ \\ '__| | | / _ \\| '__/ _ \\ \s
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/ | |___ | |_| | | |_) | __/ | | |__| (_) | | | __/ \s
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___| \\____| \\__, | |_.__/ \\___|_| \\____\\___/|_| \\___| \s
|___/ |___/ \s
SIGNER AGENT v0.1.0 SIGNER AGENT v0.1.0
============================================================ ============================================================
"""); """);
@@ -990,7 +990,8 @@ public class CcspServer {
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin); exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce"); exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400"); exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
@@ -96,6 +96,7 @@ public class CyberCoreDesktopApplication extends Application {
// Inicializa serviços centrais reais da plataforma // Inicializa serviços centrais reais da plataforma
this.certificateManager = CertificateManager.createDefault(); this.certificateManager = CertificateManager.createDefault();
this.signingService = new SigningService(this.certificateManager);
if (developmentMode) { if (developmentMode) {
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina)."); logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
@@ -57,21 +57,27 @@ public class SignaturePromptDialog {
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context, private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
String documentName, String documentType, boolean isAuthentication) { String documentName, String documentType, boolean isAuthentication) {
VBox root = new VBox(18); VBox root = new VBox(16);
root.getStyleClass().add("dialog-container"); root.getStyleClass().add("dialog-container");
root.setPrefWidth(540); root.setPadding(new Insets(20, 24, 20, 24));
root.setPrefWidth(560);
HBox headerBox = new HBox(12); HBox headerBox = new HBox(14);
headerBox.setAlignment(Pos.CENTER_LEFT); headerBox.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2); VBox titleBox = new VBox(3);
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA"); HBox.setHgrow(titleBox, Priority.ALWAYS);
titleBox.setMaxWidth(Double.MAX_VALUE);
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN" : "SOLICITAÇÃO DE ASSINATURA");
header.getStyleClass().add("dialog-header"); header.getStyleClass().add("dialog-header");
Label desc = new Label(isAuthentication Label desc = new Label(isAuthentication
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil." ? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
: "Uma aplicação web está solicitando a assinatura digital de um documento."); : "Uma aplicação web está solicitando a assinatura digital de um documento.");
desc.getStyleClass().add("brand-subtitle"); desc.getStyleClass().add("brand-subtitle");
desc.setWrapText(true);
desc.setMaxWidth(480);
titleBox.getChildren().addAll(header, desc); titleBox.getChildren().addAll(header, desc);
headerBox.getChildren().addAll( headerBox.getChildren().addAll(
@@ -145,7 +151,8 @@ public class SignaturePromptDialog {
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " + "O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente." "Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
); );
winDesc.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); winDesc.getStyleClass().add("dialog-subtitle");
winDesc.setStyle("-fx-font-size: 12px;");
winDesc.setWrapText(true); winDesc.setWrapText(true);
winBox.getChildren().addAll(winTitle, winDesc); winBox.getChildren().addAll(winTitle, winDesc);
@@ -177,6 +184,7 @@ public class SignaturePromptDialog {
if (nowVisible) { if (nowVisible) {
cbSavePin.setSelected(true); cbSavePin.setSelected(true);
} }
stage.sizeToScene();
}); });
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin); rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
@@ -184,25 +192,26 @@ public class SignaturePromptDialog {
} else { } else {
VBox inputWrapper = new VBox(8); VBox inputWrapper = new VBox(8);
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:"); Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
pinPromptLabel.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); pinPromptLabel.getStyleClass().add("dialog-subtitle");
pinPromptLabel.setStyle("-fx-font-size: 12px;");
cbSavePin.setSelected(false); cbSavePin.setSelected(false);
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin); inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
pinContainer.getChildren().add(inputWrapper); pinContainer.getChildren().add(inputWrapper);
} }
} }
detailsBox.getChildren().add(pinContainer);
} }
// Label de Alerta de Validação // Label de Alerta de Validação
Label validationLabel = new Label(); Label validationLabel = new Label();
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;"); validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
validationLabel.setVisible(false); validationLabel.setVisible(false);
validationLabel.setManaged(false);
// Botões de Ação // Botões de Ação
HBox actionsRow = new HBox(12); HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT); actionsRow.setAlignment(Pos.CENTER_RIGHT);
actionsRow.setPadding(new Insets(4, 0, 0, 0));
Button btnCancel = new Button("Cancelar"); Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-cancel"); btnCancel.getStyleClass().add("btn-cancel");
@@ -222,6 +231,8 @@ public class SignaturePromptDialog {
if (!hasStoredPin && !pinProvided) { if (!hasStoredPin && !pinProvided) {
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11."); validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
validationLabel.setVisible(true); validationLabel.setVisible(true);
validationLabel.setManaged(true);
stage.sizeToScene();
pinField.requestFocus(); pinField.requestFocus();
return; return;
} }
@@ -242,12 +253,18 @@ public class SignaturePromptDialog {
actionsRow.getChildren().addAll(btnCancel, btnConfirm); actionsRow.getChildren().addAll(btnCancel, btnConfirm);
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow); if (isA3) {
root.getChildren().addAll(headerBox, detailsBox, pinContainer, validationLabel, actionsRow);
} else {
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow);
}
Scene scene = new Scene(root); Scene scene = new Scene(root);
ThemeManager.applyTheme(scene); ThemeManager.applyTheme(scene);
stage.setScene(scene); stage.setScene(scene);
stage.sizeToScene();
stage.centerOnScreen();
stage.setOnCloseRequest(e -> approvalFuture.complete(false)); stage.setOnCloseRequest(e -> approvalFuture.complete(false));
} }
@@ -263,6 +280,8 @@ public class SignaturePromptDialog {
} }
public CompletableFuture<Boolean> showAndAwait() { public CompletableFuture<Boolean> showAndAwait() {
stage.sizeToScene();
stage.centerOnScreen();
stage.show(); stage.show();
stage.toFront(); stage.toFront();
return approvalFuture; return approvalFuture;
@@ -6,6 +6,7 @@
.root { .root {
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-text-secondary: #64748b;
} }
/* ========================================================================== /* ==========================================================================
@@ -17,6 +18,7 @@
-fx-background: #f1f5f9; -fx-background: #f1f5f9;
-fx-control-inner-background: #ffffff; -fx-control-inner-background: #ffffff;
-fx-text-fill: #0f172a; -fx-text-fill: #0f172a;
-fx-text-secondary: #64748b;
} }
.theme-light.cyber-root, .theme-light.cyber-root,
@@ -438,6 +440,7 @@
-fx-background: #0b0f17; -fx-background: #0b0f17;
-fx-control-inner-background: #131c2e; -fx-control-inner-background: #131c2e;
-fx-text-fill: #f1f5f9; -fx-text-fill: #f1f5f9;
-fx-text-secondary: #94a3b8;
} }
.theme-dark.cyber-root, .theme-dark.cyber-root,
@@ -855,7 +858,7 @@
/* Botões Base e Proteção contra Truncamento com Reticências */ /* Botões Base e Proteção contra Truncamento com Reticências */
.button { .button {
-fx-min-width: -fx-pref-size; -fx-min-width: -1;
} }
.btn-primary { .btn-primary {
@@ -865,7 +868,6 @@
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-background-radius: 6px; -fx-background-radius: 6px;
-fx-cursor: hand; -fx-cursor: hand;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1); -fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
} }
@@ -880,7 +882,6 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-background-radius: 6px; -fx-background-radius: 6px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -910,7 +911,6 @@
-fx-text-fill: #475569; -fx-text-fill: #475569;
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -933,14 +933,12 @@
/* Botões Padronizados para Diálogos e Modais */ /* Botões Padronizados para Diálogos e Modais */
.btn-dialog { .btn-dialog {
-fx-min-width: 110px; -fx-min-width: 110px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 34px; -fx-pref-height: 34px;
-fx-alignment: center; -fx-alignment: center;
} }
.btn-dialog-wide { .btn-dialog-wide {
-fx-min-width: 220px; -fx-min-width: 220px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 36px; -fx-pref-height: 36px;
-fx-alignment: center; -fx-alignment: center;
} }
@@ -949,13 +947,12 @@
.btn-compact { .btn-compact {
-fx-padding: 5px 12px; -fx-padding: 5px 12px;
-fx-font-size: 11.5px; -fx-font-size: 11.5px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
} }
/* Botão de Perigo / Exclusão / Remoção */ /* Botão de Perigo / Exclusão / Remoção */
.btn-danger { .btn-danger {
-fx-background-color: #fee2e2; -fx-background-color: #fee2e8;
-fx-text-fill: #b91c1c; -fx-text-fill: #b91c1c;
-fx-border-color: #fca5a5; -fx-border-color: #fca5a5;
-fx-border-radius: 6px; -fx-border-radius: 6px;
@@ -965,7 +962,6 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 6px 14px; -fx-padding: 6px 14px;
-fx-alignment: center; -fx-alignment: center;
-fx-min-width: -fx-pref-size;
-fx-cursor: hand; -fx-cursor: hand;
} }
+50 -18
View File
@@ -667,9 +667,9 @@
<!-- Configurações do Selo e da Assinatura --> <!-- Configurações do Selo e da Assinatura -->
<div id="pdfOptionsSection" style="margin-top: 16px; display: none; background: var(--bg-surface); padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);"> <div id="pdfOptionsSection" style="margin-top: 16px; display: none; background: var(--bg-surface); padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
<h4 style="font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 12px;">🎨 Configurações do Selo Visual</h4> <h4 style="font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 12px;">🎨 Configurações do Selo Visual</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px;"> <div style="display: grid; grid-template-columns: minmax(240px, 360px); gap: 14px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">Tema do Selo do Agente:</label> <label class="form-label">Tema do Selo Visual:</label>
<select id="pdfSealThemeSelect" class="cert-bar-select"> <select id="pdfSealThemeSelect" class="cert-bar-select">
<option value="icp-brasil">ICP-Brasil Institucional (Padrão)</option> <option value="icp-brasil">ICP-Brasil Institucional (Padrão)</option>
<option value="gov-br">Governamental (Gov.br / ITI)</option> <option value="gov-br">Governamental (Gov.br / ITI)</option>
@@ -678,16 +678,6 @@
<option value="custom">🎨 Selo Customizado do Designer</option> <option value="custom">🎨 Selo Customizado do Designer</option>
</select> </select>
</div> </div>
<div class="form-group">
<label class="form-label">Razão / Motivo da Assinatura:</label>
<input type="text" id="pdfReasonInput" value="Documento aprovado e assinado digitalmente" placeholder="Ex: De acordo com os termos">
</div>
<div class="form-group">
<label class="form-label">Localização:</label>
<input type="text" id="pdfLocationInput" value="Curitiba, PR - Brasil" placeholder="Ex: Brasília, DF">
</div>
</div> </div>
<div style="margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap;"> <div style="margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap;">
@@ -1062,6 +1052,13 @@
certTypeBadge.textContent = cert.type || 'A1'; certTypeBadge.textContent = cert.type || 'A1';
certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`; certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`;
certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`; certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`;
if (pdfStampPicker) {
pdfStampPicker.updateSealTheme(null, {
signerName: cert.common_name || cert.alias,
signerDoc: cert.cpf_cnpj ? `CPF: ${cert.cpf_cnpj}` : (cert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil')
});
}
} else { } else {
certTypeBadge.style.display = 'none'; certTypeBadge.style.display = 'none';
certValidityText.textContent = ''; certValidityText.textContent = '';
@@ -1171,22 +1168,56 @@
stampPickerMount.style.display = 'block'; stampPickerMount.style.display = 'block';
pdfOptionsSection.style.display = 'block'; pdfOptionsSection.style.display = 'block';
const activeCert = currentCertificates.find(c => c.id === selectedCertificateId);
const currentThemeVal = pdfSealThemeSelect ? pdfSealThemeSelect.value : 'icp-brasil';
const initialTheme = (currentThemeVal === 'custom' && currentCustomSealConfig)
? currentCustomSealConfig
: currentThemeVal;
const rawCertName = activeCert ? (activeCert.common_name || activeCert.alias) : '';
const cleanCertName = rawCertName.includes(':') ? rawCertName.split(':')[0].trim() : rawCertName;
const initialSignerInfo = activeCert ? {
signerName: cleanCertName,
signerDoc: activeCert.cpf_cnpj ? `CPF: ${activeCert.cpf_cnpj}` : (activeCert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil')
} : {
signerName: 'Nome do Titular do Certificado',
signerDoc: 'CPF: ***.456.789-**'
};
if (!pdfStampPicker) { if (!pdfStampPicker) {
pdfStampPicker = new PdfStampPicker({ pdfStampPicker = new PdfStampPicker({
container: stampPickerMount, container: stampPickerMount,
pdfSource: buffer, pdf: buffer,
sealTheme: initialTheme,
signerInfo: initialSignerInfo,
initialScale: 1.0, initialScale: 1.0,
onPositionChange: (pos) => { onPositionChange: (pos) => {
console.log('Posição do selo atualizada:', pos); console.log('Posição do selo atualizada:', pos);
},
onSignRequest: () => {
btnSignPdfFull.click();
} }
}); });
} else { } else {
pdfStampPicker.updateSealTheme(initialTheme, initialSignerInfo);
pdfStampPicker.loadPdf(buffer); pdfStampPicker.loadPdf(buffer);
} }
updateButtonStates(); updateButtonStates();
} }
// Sincroniza alteração de tema com o selo sobre o PDF
pdfSealThemeSelect.addEventListener('change', () => {
const themeVal = pdfSealThemeSelect.value;
if (pdfStampPicker) {
if (themeVal === 'custom' && currentCustomSealConfig) {
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
} else {
pdfStampPicker.updateSealTheme(themeVal);
}
}
});
// Assinatura Completa de PDF (Devolve PDF Assinado Base64) // Assinatura Completa de PDF (Devolve PDF Assinado Base64)
btnSignPdfFull.addEventListener('click', async () => { btnSignPdfFull.addEventListener('click', async () => {
btnSignPdfFull.disabled = true; btnSignPdfFull.disabled = true;
@@ -1201,13 +1232,11 @@
const signParams = { const signParams = {
certificateId: selectedCertificateId, certificateId: selectedCertificateId,
pdf: currentPdfBytes, pdf: currentPdfFile || currentPdfBytes,
page: stampConfig.page, page: stampConfig.page,
x: stampConfig.x, x: stampConfig.x,
y: stampConfig.y, y: stampConfig.y,
scale: stampConfig.scale, scale: stampConfig.scale
reason: document.getElementById('pdfReasonInput').value || 'Assinado Digitalmente',
location: document.getElementById('pdfLocationInput').value || 'Brasil'
}; };
if (selectedTheme === 'custom' && currentCustomSealConfig) { if (selectedTheme === 'custom' && currentCustomSealConfig) {
@@ -1324,9 +1353,12 @@
document.getElementById('btnApplySealToPdf').addEventListener('click', () => { document.getElementById('btnApplySealToPdf').addEventListener('click', () => {
currentCustomSealConfig = pdfSealDesigner.getConfig(); currentCustomSealConfig = pdfSealDesigner.getConfig();
pdfSealThemeSelect.value = 'custom'; pdfSealThemeSelect.value = 'custom';
if (pdfStampPicker) {
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
}
// Alterna para aba PDF // Alterna para aba PDF
document.querySelector('[data-tab="tab-pdf"]').click(); document.querySelector('[data-tab="tab-pdf"]').click();
alert('Selo customizado selecionado para a assinatura do PDF!'); alert('Selo customizado selecionado e aplicado ao posicionador de PDF!');
}); });
} }
+49 -7
View File
@@ -209,6 +209,9 @@ export class PdfSealDesigner {
padding: 20px; padding: 20px;
color: #f8fafc; color: #f8fafc;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
} }
.designer-header { .designer-header {
margin-bottom: 20px; margin-bottom: 20px;
@@ -227,12 +230,13 @@ export class PdfSealDesigner {
} }
.designer-body { .designer-body {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 24px; gap: 24px;
min-width: 0;
} }
@media (max-width: 860px) { @media (max-width: 860px) {
.designer-body { .designer-body {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
} }
} }
.form-section-title { .form-section-title {
@@ -364,12 +368,17 @@ export class PdfSealDesigner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
min-width: 0;
max-width: 100%;
} }
.preview-card, .json-export-card { .preview-card, .json-export-card {
background: #131c31; background: #131c31;
border: 1px solid #24304f; border: 1px solid #24304f;
border-radius: 10px; border-radius: 10px;
padding: 16px; padding: 16px;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
} }
.preview-card h4, .json-export-card h4 { .preview-card h4, .json-export-card h4 {
font-size: 12px; font-size: 12px;
@@ -385,9 +394,14 @@ export class PdfSealDesigner {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.3); box-shadow: 0 4px 15px rgba(0,0,0,0.3);
max-width: 340px;
margin: 0 auto;
box-sizing: border-box;
overflow: hidden;
} }
.seal-live-preview-box svg { .seal-live-preview-box svg {
max-width: 100%; max-width: 100%;
width: 100%;
height: auto; height: auto;
display: block; display: block;
} }
@@ -426,8 +440,22 @@ export class PdfSealDesigner {
font-size: 10.5px; font-size: 10.5px;
color: #38bdf8; color: #38bdf8;
max-height: 180px; max-height: 180px;
overflow: auto; overflow-y: auto;
overflow-x: hidden;
margin: 0; margin: 0;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
}
.theme-json-box code {
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
display: block;
max-width: 100%;
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
@@ -541,7 +569,7 @@ export class PdfSealDesigner {
let iconGraphic = ''; let iconGraphic = '';
if (t.customLogoBase64) { if (t.customLogoBase64) {
iconGraphic = `<image href="${t.customLogoBase64}" x="12" y="14" width="34" height="34" preserveAspectRatio="xMidYMid meet" />`; iconGraphic = `<image xlink:href="${t.customLogoBase64}" href="${t.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
} else { } else {
switch (t.iconStyle) { switch (t.iconStyle) {
case 'star': case 'star':
@@ -578,7 +606,7 @@ export class PdfSealDesigner {
} }
const svg = ` const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82" style="border-radius:6px; background:${t.backgroundColor};"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320 82" width="320" height="82" style="border-radius:6px; background:${t.backgroundColor};">
<rect x="1" y="1" width="318" height="80" rx="6" fill="${t.backgroundColor}" stroke="${t.borderColor}" stroke-width="1.5"/> <rect x="1" y="1" width="318" height="80" rx="6" fill="${t.backgroundColor}" stroke="${t.borderColor}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="3" fill="${t.primaryColor}"/> <rect x="1" y="1" width="6" height="80" rx="3" fill="${t.primaryColor}"/>
@@ -591,13 +619,20 @@ export class PdfSealDesigner {
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text> <text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text> <text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text> <text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[dateText, 'AC SERPRO v5'].filter(Boolean).join(' • ')}</text> <text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${dateText ? `Data: ${dateText}` : ''}</text>
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text> <text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
</svg> </svg>
`; `;
this.previewBox.innerHTML = svg; this.previewBox.innerHTML = svg;
this.jsonBox.textContent = JSON.stringify(this.getThemeConfig(), null, 2);
// Na visualização do código, trunca se o Base64 for gigante para preservar performance da UI
const displayConfig = { ...this.getThemeConfig() };
if (displayConfig.customLogoBase64 && displayConfig.customLogoBase64.length > 100) {
const lenKb = (displayConfig.customLogoBase64.length / 1024).toFixed(1);
displayConfig.customLogoBase64 = `${displayConfig.customLogoBase64.substring(0, 32)}... [${lenKb} KB]`;
}
this.jsonBox.textContent = JSON.stringify(displayConfig, null, 2);
} }
/** /**
@@ -625,6 +660,13 @@ export class PdfSealDesigner {
}; };
} }
/**
* Alias conveniente para getThemeConfig().
*/
getConfig() {
return this.getThemeConfig();
}
/** /**
* Carrega um tema para edição. * Carrega um tema para edição.
*/ */
+234 -71
View File
@@ -25,7 +25,11 @@ export class PdfStampPicker {
this.onPositionChange = options.onPositionChange || null; this.onPositionChange = options.onPositionChange || null;
this.onSignRequest = options.onSignRequest || null; this.onSignRequest = options.onSignRequest || null;
this.sealTheme = options.sealTheme || null; this.sealTheme = options.sealTheme || 'icp-brasil';
this.signerInfo = options.signerInfo || {
signerName: 'Nome do Titular do Certificado',
signerDoc: 'CPF: ***.456.789-**'
};
// Estado interno // Estado interno
this.pdfDocument = null; this.pdfDocument = null;
@@ -42,8 +46,8 @@ export class PdfStampPicker {
// Estado do selo // Estado do selo
this.sealScale = options.initialScale || 1.0; this.sealScale = options.initialScale || 1.0;
this.baseSealWidth = 230; // pixels base no visualizador this.baseSealWidth = 240; // Proporção 320x82 pt
this.baseSealHeight = 60; this.baseSealHeight = 61.5;
this.sealPosX = null; // pixel relativo ao container da folha this.sealPosX = null; // pixel relativo ao container da folha
this.sealPosY = null; this.sealPosY = null;
this.isDragging = false; this.isDragging = false;
@@ -98,20 +102,9 @@ export class PdfStampPicker {
<div class="picker-sheet-wrapper"> <div class="picker-sheet-wrapper">
<div class="sheet-container"> <div class="sheet-container">
<canvas class="pdf-canvas"></canvas> <canvas class="pdf-canvas"></canvas>
<!-- Selo Visual Interativo (Drag & Drop) --> <!-- Selo Visual Interativo Oficial (Drag & Drop) -->
<div class="stamp-overlay" style="display:none;"> <div class="stamp-overlay" style="display:none;" title="Clique e arraste para posicionar o selo na folha">
<div class="stamp-content"> <div class="stamp-svg-container"></div>
<div class="stamp-icon-box">
<svg viewBox="0 0 24 24" class="stamp-svg-icon" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-1 6h2v2h-2V7zm0 4h2v6h-2v-6z"/>
</svg>
</div>
<div class="stamp-info-box">
<div class="stamp-badge-text">ASSINADO DIGITALMENTE • ICP-BRASIL</div>
<div class="stamp-signer-name">NOME DO TITULAR DO CERTIFICADO</div>
<div class="stamp-signer-meta">CPF: ***.456.789-** • VALIDADE JURÍDICA</div>
</div>
</div>
<div class="stamp-resize-handle" title="Arraste para redimensionar o selo"></div> <div class="stamp-resize-handle" title="Arraste para redimensionar o selo"></div>
</div> </div>
</div> </div>
@@ -208,6 +201,8 @@ export class PdfStampPicker {
this.payloadCode = this.root.querySelector('.payload-code-box code'); this.payloadCode = this.root.querySelector('.payload-code-box code');
this.btnCopyPayload = this.root.querySelector('.btn-copy-payload'); this.btnCopyPayload = this.root.querySelector('.btn-copy-payload');
this.btnTriggerSign = this.root.querySelector('.btn-trigger-sign'); this.btnTriggerSign = this.root.querySelector('.btn-trigger-sign');
this._renderSealGraphic();
} }
_injectStyles() { _injectStyles() {
@@ -291,21 +286,22 @@ export class PdfStampPicker {
} }
.picker-body { .picker-body {
display: flex; display: flex;
min-height: 520px; min-height: 560px;
height: 600px; height: 680px;
} }
.picker-viewport { .picker-viewport {
flex: 1; flex: 1;
background: #060911; background: #060911;
overflow: auto; overflow: auto;
position: relative; position: relative;
display: flex; padding: 30px;
align-items: center; display: block;
justify-content: center; box-sizing: border-box;
padding: 24px;
} }
.picker-sheet-wrapper { .picker-sheet-wrapper {
display: none; display: none;
margin: 0 auto;
width: fit-content;
box-shadow: 0 12px 35px rgba(0,0,0,0.65); box-shadow: 0 12px 35px rgba(0,0,0,0.65);
border: 1px solid rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1);
background: #ffffff; background: #ffffff;
@@ -322,64 +318,53 @@ export class PdfStampPicker {
} }
.stamp-overlay { .stamp-overlay {
position: absolute; position: absolute;
width: 230px; width: 240px;
height: 60px; height: 61.5px;
border: 2px dashed #0284c7; border: 1.5px solid #0284c7;
background: rgba(13, 71, 161, 0.08); background: transparent;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 4px 15px rgba(0,0,0,0.25); box-shadow: 0 4px 18px rgba(0,0,0,0.3);
cursor: grab; cursor: grab;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
z-index: 20; z-index: 20;
user-select: none;
transition: box-shadow 0.2s, border-color 0.2s; transition: box-shadow 0.2s, border-color 0.2s;
} }
.stamp-overlay:hover {
border-color: #38bdf8;
box-shadow: 0 6px 22px rgba(56, 189, 248, 0.4);
}
.stamp-overlay:active { .stamp-overlay:active {
cursor: grabbing; cursor: grabbing;
border-color: #38bdf8; border-color: #0ea5e9;
box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4); box-shadow: 0 8px 26px rgba(14, 165, 233, 0.55);
} }
.stamp-content { .stamp-svg-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: block;
align-items: center;
padding: 4px 8px;
box-sizing: border-box;
gap: 8px;
pointer-events: none; pointer-events: none;
} }
.stamp-icon-box { .stamp-svg-container svg {
width: 34px;
height: 34px;
color: #0d47a1;
flex-shrink: 0;
}
.stamp-svg-icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
border-radius: 6px;
} }
.stamp-info-box { .stamp-resize-handle {
display: flex; position: absolute;
flex-direction: column; bottom: 0;
justify-content: center; right: 0;
line-height: 1.15; width: 14px;
height: 14px;
background: linear-gradient(135deg, transparent 50%, #0284c7 50%);
cursor: se-resize;
z-index: 25;
border-bottom-right-radius: 5px;
} }
.stamp-badge-text { .stamp-resize-handle:hover {
font-size: 7.5px; background: linear-gradient(135deg, transparent 50%, #38bdf8 50%);
font-weight: 800;
color: #0d47a1;
letter-spacing: 0.3px;
}
.stamp-signer-name {
font-size: 10px;
font-weight: 800;
color: #0f172a;
margin: 1px 0;
}
.stamp-signer-meta {
font-size: 7.5px;
color: #475569;
} }
.picker-empty-state { .picker-empty-state {
text-align: center; text-align: center;
@@ -676,14 +661,16 @@ export class PdfStampPicker {
this.pdfBase64 = this._bufferToBase64(arrayBuffer); this.pdfBase64 = this._bufferToBase64(arrayBuffer);
} }
this.pdfBytes = new Uint8Array(arrayBuffer); this.pdfBytes = new Uint8Array(arrayBuffer.slice(0));
this.docNameLabel.textContent = this.documentName; this.docNameLabel.textContent = this.documentName;
this.docNameLabel.title = this.documentName; this.docNameLabel.title = this.documentName;
// Garante suporte ao PDF.js // Garante suporte ao PDF.js
await this._ensurePdfJsLoaded(); await this._ensurePdfJsLoaded();
const loadingTask = window.pdfjsLib.getDocument({ data: this.pdfBytes }); // Envia uma fatia clonada ao Worker do PDF.js para não descolar (detach) o buffer original
const workerData = new Uint8Array(arrayBuffer.slice(0));
const loadingTask = window.pdfjsLib.getDocument({ data: workerData });
this.pdfDocument = await loadingTask.promise; this.pdfDocument = await loadingTask.promise;
this.totalPages = this.pdfDocument.numPages; this.totalPages = this.pdfDocument.numPages;
this.totalPagesLabel.textContent = this.totalPages; this.totalPagesLabel.textContent = this.totalPages;
@@ -740,6 +727,15 @@ export class PdfStampPicker {
const ctx = this.canvas.getContext('2d'); const ctx = this.canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport: viewport }).promise; await page.render({ canvasContext: ctx, viewport: viewport }).promise;
// Ajusta o tamanho base do selo (320x82 pt) proporcionalmente aos pixels da tela
const pixelScale = viewport.width / this.pagePdfWidth;
this.baseSealWidth = 320 * pixelScale;
this.baseSealHeight = 82 * pixelScale;
const stampW = this.baseSealWidth * this.sealScale;
const stampH = this.baseSealHeight * this.sealScale;
this.stampOverlay.style.width = `${stampW}px`;
this.stampOverlay.style.height = `${stampH}px`;
this.stampOverlay.style.display = 'block'; this.stampOverlay.style.display = 'block';
// Recalcula limites se já houver posição definida // Recalcula limites se já houver posição definida
@@ -881,9 +877,7 @@ export class PdfStampPicker {
scale: Number(this.sealScale.toFixed(2)), scale: Number(this.sealScale.toFixed(2)),
visible_seal: true, visible_seal: true,
seal_theme_id: this.sealTheme?.id || 'icp-brasil', seal_theme_id: this.sealTheme?.id || 'icp-brasil',
seal_custom: this.sealTheme || null, seal_custom: typeof this.sealTheme === 'object' ? this.sealTheme : null,
reason: 'Assinatura Digital ICP-Brasil',
location: 'Brasil',
pdf: this.pdfBase64 ? `${this.pdfBase64.substring(0, 32)}...[${this.pdfBytes?.length || 0} bytes]` : null pdf: this.pdfBase64 ? `${this.pdfBase64.substring(0, 32)}...[${this.pdfBytes?.length || 0} bytes]` : null
}; };
} }
@@ -904,13 +898,182 @@ export class PdfStampPicker {
scale: Number(this.sealScale.toFixed(2)), scale: Number(this.sealScale.toFixed(2)),
visible_seal: true, visible_seal: true,
seal_theme_id: this.sealTheme?.id || 'icp-brasil', seal_theme_id: this.sealTheme?.id || 'icp-brasil',
seal_custom: this.sealTheme || null, seal_custom: typeof this.sealTheme === 'object' ? this.sealTheme : null,
reason: 'Assinatura Digital ICP-Brasil',
location: 'Brasil',
pdf: this.pdfBase64 pdf: this.pdfBase64
}; };
} }
/**
* Retorna os dados geométricos essenciais do posicionamento do selo.
*/
getStampConfig() {
return {
page: this.currentPageNumber,
x: this.calculatedPdfX,
y: this.calculatedPdfY,
scale: Number(this.sealScale.toFixed(2)),
position: this.selectedPositionEnum
};
}
/**
* Atualiza o tema visual ou os dados do titular estampados no selo em tempo real.
* @param {string|Object} [theme] ID do tema ('icp-brasil', 'gov-br', etc.) ou objeto completo de tema
* @param {Object} [signerInfo] Dados do titular: { signerName, signerDoc }
*/
updateSealTheme(theme, signerInfo) {
if (theme) {
this.sealTheme = theme;
}
if (signerInfo) {
this.signerInfo = { ...(this.signerInfo || {}), ...signerInfo };
}
this._renderSealGraphic();
}
_renderSealGraphic() {
const theme = typeof this.sealTheme === 'object' && this.sealTheme !== null
? this.sealTheme
: this._getPresetTheme(this.sealTheme);
const signer = this.signerInfo || {};
const rawName = signer.signerName || 'Nome do Titular do Certificado';
// Remove sufixo :CPF ou :CNPJ para exibir apenas o nome limpo do titular, idêntico ao assinador Java
const name = rawName.includes(':') ? rawName.split(':')[0].trim() : rawName.trim();
const doc = signer.signerDoc || 'CPF: ***.456.789-**';
const dateText = new Date().toLocaleDateString('pt-BR') + ' ' + new Date().toLocaleTimeString('pt-BR');
// Ajusta o tamanho da fonte proporcionalmente ao comprimento do nome
let nameFontSize = 11.5;
if (name.length > 36) nameFontSize = 8.8;
else if (name.length > 26) nameFontSize = 10.0;
let iconGraphic = '';
if (theme.customLogoBase64) {
iconGraphic = `<image xlink:href="${theme.customLogoBase64}" href="${theme.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
} else {
switch (theme.iconStyle || theme.id) {
case 'star':
case 'gov-br':
iconGraphic = `
<circle cx="28" cy="30" r="15" fill="${theme.primaryColor}" opacity="0.12" />
<circle cx="28" cy="30" r="15" fill="none" stroke="${theme.primaryColor}" stroke-width="1.5" />
<path d="M28 18 L31 26 L39 26 L33 31 L35 39 L28 34 L21 39 L23 31 L17 26 L25 26 Z" fill="${theme.secondaryColor || '#fbbf24'}" />
`;
break;
case 'crystal':
case 'cybercore-modern':
iconGraphic = `
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="${theme.primaryColor}" opacity="0.15" />
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="none" stroke="${theme.primaryColor}" stroke-width="1.5" />
<line x1="28" y1="14" x2="28" y2="46" stroke="${theme.secondaryColor || '#10b981'}" stroke-width="1.5" />
`;
break;
case 'notary':
case 'minimal-clean':
iconGraphic = `
<circle cx="28" cy="30" r="16" fill="none" stroke="${theme.primaryColor}" stroke-width="2" stroke-dasharray="3,2" />
<circle cx="28" cy="30" r="12" fill="${theme.primaryColor}" opacity="0.1" />
<path d="M22 30 L26 34 L34 26" fill="none" stroke="${theme.secondaryColor || '#64748b'}" stroke-width="2" stroke-linecap="round" />
`;
break;
case 'shield':
case 'icp-brasil':
default:
iconGraphic = `
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="${theme.primaryColor}" opacity="0.12" />
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="none" stroke="${theme.primaryColor}" stroke-width="1.8" />
<rect x="23" y="27" width="10" height="8" rx="2" fill="${theme.secondaryColor || theme.primaryColor}" />
<path d="M25 27 V23.5 C25 21.8 26.3 20.5 28 20.5 C29.7 20.5 31 21.8 31 23.5 V27" fill="none" stroke="${theme.secondaryColor || theme.primaryColor}" stroke-width="1.5" />
`;
break;
}
}
const header = theme.customHeader || (theme.id === 'cybercore-modern' ? 'CYBERCORE SIGNER • ASSINATURA ELETRÔNICA' : (theme.id === 'minimal-clean' ? 'Assinado Digitalmente' : 'DOCUMENTO ASSINADO DIGITALMENTE'));
const footer = theme.customFooter || (theme.id === 'minimal-clean' ? 'Conforme MP 2.200-2/2001 e ICP-Brasil' : 'ICP-Brasil • MP nº 2.200-2/2001');
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320 82" width="100%" height="100%" style="background:${theme.backgroundColor || '#ffffff'}; border-radius:6px;">
<rect x="1" y="1" width="318" height="80" rx="6" fill="${theme.backgroundColor || '#ffffff'}" stroke="${theme.borderColor || '#cbd5e1'}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="3" fill="${theme.primaryColor || '#0d47a1'}"/>
<!-- Emblema / Brasão -->
<g id="seal-icon-group">
${iconGraphic}
</g>
<!-- Tipografia do Selo -->
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${theme.primaryColor || '#0d47a1'}" letter-spacing="0.3">${header}</text>
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="${nameFontSize}" font-weight="bold" fill="${theme.textColor || '#0f172a'}">${name}</text>
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${doc}</text>
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">Data: ${dateText}</text>
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${footer}</text>
</svg>
`;
if (this.stampOverlay) {
const container = this.stampOverlay.querySelector('.stamp-svg-container');
if (container) {
container.innerHTML = svg;
}
}
}
_getPresetTheme(themeId) {
switch (themeId) {
case 'gov-br':
return {
id: 'gov-br',
primaryColor: '#005696',
secondaryColor: '#ffcc00',
backgroundColor: '#ffffff',
borderColor: '#93c5fd',
textColor: '#0f172a',
iconStyle: 'star',
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE • GOV.BR',
customFooter: 'Conforme Decreto nº 10.543/2020 e ICP-Brasil'
};
case 'cybercore-modern':
return {
id: 'cybercore-modern',
primaryColor: '#0284c7',
secondaryColor: '#10b981',
backgroundColor: '#ffffff',
borderColor: '#38bdf8',
textColor: '#0f172a',
iconStyle: 'crystal',
customHeader: 'CYBERCORE SIGNER • ASSINATURA ELETRÔNICA',
customFooter: 'Validação criptográfica com fé pública'
};
case 'minimal-clean':
return {
id: 'minimal-clean',
primaryColor: '#334155',
secondaryColor: '#64748b',
backgroundColor: '#fafafa',
borderColor: '#cbd5e1',
textColor: '#1e293b',
iconStyle: 'notary',
customHeader: 'Assinado Digitalmente',
customFooter: 'Conforme MP 2.200-2/2001 e ICP-Brasil'
};
case 'icp-brasil':
default:
return {
id: 'icp-brasil',
primaryColor: '#0d47a1',
secondaryColor: '#0284c7',
backgroundColor: '#ffffff',
borderColor: '#93c5fd',
textColor: '#0f172a',
iconStyle: 'shield',
customHeader: 'ASSINADO DIGITALMENTE • ICP-BRASIL',
customFooter: 'ICP-Brasil • MP nº 2.200-2/2001'
};
}
}
_bufferToBase64(buffer) { _bufferToBase64(buffer) {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
let binary = ''; let binary = '';
+64 -1
View File
@@ -691,8 +691,71 @@ export class CyberCoreSigner {
// Métodos Utilitários Internos // Métodos Utilitários Internos
// ========================================================================= // =========================================================================
/**
* Executa requisições HTTP REST ao servidor local CCSP/1.
*
* @param {string} endpoint Caminho do endpoint (ex: '/ccsp/v1/status').
* @param {Object} [options={}] Opções do fetch (method, body, timeoutMs, etc).
* @returns {Promise<Object>} Resposta JSON decodificada.
*/
async _fetch(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const timeout = options.timeoutMs || 15000;
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timeoutId = controller ? setTimeout(() => controller.abort(), timeout) : null;
try {
const headers = {
'Accept': 'application/json',
'X-CyberCore-AppId': this.applicationId,
...(options.headers || {})
};
if (options.body && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
}
const fetchOptions = {
...options,
headers
};
if (controller) {
fetchOptions.signal = controller.signal;
}
const res = await fetch(url, fetchOptions);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorCode = data.error_code || `HTTP_${res.status}`;
const message = data.message || `Erro retornado pelo agente (${res.status})`;
this._throwMappedError(errorCode, message);
}
return data;
} catch (err) {
if (err.name === 'AbortError') {
throw new CyberCoreError(`Tempo limite esgotado (${timeout}ms) na comunicação com o agente.`, 'TIMEOUT_ERROR');
}
if (err instanceof CyberCoreError) {
throw err;
}
throw new AgentNotRunningError(`Falha ao conectar com o CyberCore Signer em ${this.baseUrl}: ${err.message}`);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
async _fileToBuffer(file) { async _fileToBuffer(file) {
if (file instanceof ArrayBuffer) return file; if (file instanceof ArrayBuffer) {
if (file.byteLength === 0) {
throw new Error('O buffer binário está vazio ou foi desacoplado (detached). Forneça o objeto File ou Blob original.');
}
return file;
}
if (ArrayBuffer.isView(file)) { if (ArrayBuffer.isView(file)) {
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
} }