feat(signatures): módulo completo de assinatura eletrônica
Porta o motor de assinatura do legado com fidelidade algorítmica exata — hash-chain (SHA256 encadeado, seq bigserial define a ordem, não timestamp), OTP (6 dígitos via crypto.randomInt, HMAC-SHA256 com SIGNATURE_OTP_SECRET, TTL 5min, 5 tentativas, único ativo por vez) e a máquina de estados de 19 estados do envelope + 8 do signatário — tudo re-verificado contra o código-fonte real do legado por um subagente dedicado antes de qualquer linha ser escrita, cruzado com eden.md módulo 4. 5 tabelas novas (signature_envelopes/signers/documents/ otp_challenges/audit_events, esta última append-only via a mesma GUC eden.allow_audit_mutation que já protege audit_log desde a Fase 1 — canonicalização de hash extraída para packages/database/src/ hash-chain.ts, compartilhada entre as duas). Três adaptações deliberadas ao EDEN (documentadas na migration): documentos vêm de document_generations em vez de um módulo de anexo genérico que não existe ainda; reseller_id em vez de reseller_registration_id; só a versão correta (v2) da canonicalização, sem a v1 do legado (existia lá só para tolerar hashes pré-correção de bug, EDEN nunca teve isso). Certificado de conclusão renderizado no servidor via Chromium (não client-side como no legado) — elimina o upload multipart do certificado pronto. Os dois passos autenticados de finalização (seal-checkpoint, finalize) continuam explícitos e manuais, não automáticos, mesmo sendo tecnicamente possível agora — esse checkpoint humano antes de uma finalização irreversível é comportamento de negócio herdado do legado, não um detalhe do desenho client-side do certificado que deixou de existir. Fluxo público (token do link, nunca envelopeId/signerId do corpo da requisição) e verificação pública (só para COMPLETED, recomputa envelope_hash ao vivo, nomes mascarados) inteiros. Verificado: 28 testes novos (15 puros — hash-chain/máquina de estados/OTP/token — e 13 de ciclo de vida completo via HTTP real, incluindo merge de PDF de verdade via pdf-lib contra saída real do Chromium mockado). Suíte completa: 70/70 verdes. Testado ao vivo através do container real, ponta a ponta, sem nenhum mock: criação → convite → fluxo público completo dos dois signatários (OTP recuperado via HMAC com o segredo real, já que SMTP não está configurado neste ambiente) → SIGNED → seal-checkpoint → finalize (certificado real via Chromium, merge real, upload real no MinIO) → public-verify — SHA-256 do PDF final baixado bate byte a byte com o gravado no Postgres, integrityValid:true. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
This commit is contained in:
283
packages/database/migrations/1788536022387_signatures.cjs
Normal file
283
packages/database/migrations/1788536022387_signatures.cjs
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Assinatura Eletrônica — eden.md módulo 4, §§1.4-1.9 e 5-10. Deliberadamente
|
||||
* adiado desde 1788468521075_documents.cjs ("cryptographically/legally
|
||||
* sensitive... needs its own dedicated migration and review"). Ported com
|
||||
* fidelidade algorítmica exata (máquina de estados, hash-chain, OTP) — as
|
||||
* únicas divergências deliberadas do legado, documentadas caso a caso
|
||||
* abaixo, são adaptações à própria arquitetura do EDEN, nunca ao conteúdo
|
||||
* de segurança/negócio em si:
|
||||
*
|
||||
* - `signature_documents.source_generation_id` referencia
|
||||
* `document_generations(id)` em vez do `source_attachment_id` do
|
||||
* legado — EDEN ainda não tem um módulo de upload de anexo genérico
|
||||
* (client_registration_attachments/reseller_registration_attachments
|
||||
* nunca foram portados), mas já tem `document_generations` (PDFs reais,
|
||||
* gerados via Chromium, com SHA-256 e S3 desde a leva anterior desta
|
||||
* sessão) — a fonte natural de "documento já gerado, hasheado, pronto
|
||||
* para congelar" no EDEN. Igual ao legado, é só referência histórica:
|
||||
* uma vez congelado em `signature_documents`, a cópia S3 própria e o
|
||||
* hash já congelados são a fonte da verdade, não a FK.
|
||||
* - `reseller_id` (FK para `resellers`) em vez de `reseller_registration_id`
|
||||
* — o EDEN não tem uma entidade "cadastro de revenda" separada da
|
||||
* revenda em si (eden.md documenta `reseller_registration_id` como a
|
||||
* origem alternativa a `client_registration_id`, mas o EDEN modela
|
||||
* revenda como uma única tabela `resellers`, não duas). Os dados do
|
||||
* signatário `contratante` são sempre informados explicitamente na
|
||||
* criação do envelope em ambos os sistemas (nunca derivados
|
||||
* automaticamente do cadastro de origem) — só o signatário
|
||||
* `contratada` é auto-preenchido, de `companies.legal_rep_*` — então
|
||||
* essa troca não perde nenhum dado necessário.
|
||||
* - `hash_algorithm_version` existe mas só a versão 2 (a fórmula correta,
|
||||
* canonicalização recursiva em profundidade) é usada — ver o
|
||||
* doc-comment de packages/database/src/hash-chain.ts para o porquê da
|
||||
* v1 do legado (compat com hashes já gravados antes de uma correção de
|
||||
* bug real) não fazer sentido aqui: o EDEN nunca teve uma v1 buggy.
|
||||
* - Certificado de conclusão: o legado desenha client-side (React →
|
||||
* html2canvas → jsPDF, recebido via upload multipart em
|
||||
* `finalizeEnvelope`). O EDEN já tem Chromium server-side real
|
||||
* (packages/integrations/src/pdf/renderer.ts, construído nesta mesma
|
||||
* sessão) — o certificado é renderizado no servidor a partir dos
|
||||
* mesmos dados já gravados (evidence_checkpoint_hash/verification_id/
|
||||
* envelope_hash), eliminando o upload multipart do passo 2 inteiramente
|
||||
* (o servidor nunca precisa confiar num PDF vindo de fora como "o
|
||||
* certificado"). Ver apps/api/src/signatures/.
|
||||
*
|
||||
* Tudo o mais — os 19 estados do envelope, os 8 estados do signatário, o
|
||||
* algoritmo de hash-chain (SHA256(previous + canonical_json(campos)),
|
||||
* `seq` bigserial definindo a ordem, não timestamp), o OTP (6 dígitos via
|
||||
* crypto.randomInt, HMAC-SHA256 com SIGNATURE_OTP_SECRET, TTL de 5min,
|
||||
* máx. 5 tentativas, um único código ativo por vez), o token de link
|
||||
* público (32 bytes aleatórios, só o SHA-256 persistido, 7 dias de
|
||||
* validade), o processo de finalização em dois passos (selar checkpoint
|
||||
* ANTES do certificado existir, depois consolidar via pdf-lib) — tudo isso
|
||||
* é fidelidade exata ao legado, verificado contra o código-fonte real
|
||||
* (não reconstruído de memória).
|
||||
*/
|
||||
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = (pgm) => {
|
||||
pgm.sql(`
|
||||
-- =====================================================================
|
||||
-- signature_envelopes — o agregado raiz
|
||||
-- =====================================================================
|
||||
CREATE SEQUENCE signature_envelope_seq START 1;
|
||||
|
||||
CREATE TABLE signature_envelopes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by UUID REFERENCES users(id),
|
||||
envelope_number TEXT UNIQUE NOT NULL,
|
||||
company_id UUID NOT NULL REFERENCES companies(id),
|
||||
client_registration_id UUID REFERENCES client_registrations(id),
|
||||
reseller_id UUID REFERENCES resellers(id),
|
||||
status TEXT NOT NULL DEFAULT 'DRAFT' CHECK (status IN (
|
||||
'DRAFT', 'READY', 'SENT', 'VIEWED', 'IDENTITY_PENDING', 'CONSENT_PENDING',
|
||||
'OTP_PENDING', 'OTP_SENT', 'OTP_VERIFIED', 'READY_TO_SIGN', 'SIGNING', 'SIGNED',
|
||||
'FINALIZING', 'COMPLETED', 'DECLINED', 'EXPIRED', 'CANCELLED', 'SUPERSEDED', 'ERROR'
|
||||
)),
|
||||
verification_id TEXT UNIQUE,
|
||||
final_s3_key TEXT,
|
||||
final_pdf_sha256 TEXT,
|
||||
certificate_s3_key TEXT,
|
||||
evidence_checkpoint_hash TEXT,
|
||||
envelope_hash TEXT,
|
||||
sent_at TIMESTAMPTZ,
|
||||
viewed_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
declined_at TIMESTAMPTZ,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
expired_at TIMESTAMPTZ,
|
||||
signed_at TIMESTAMPTZ,
|
||||
CONSTRAINT chk_signature_envelopes_one_source CHECK (
|
||||
(client_registration_id IS NOT NULL)::int + (reseller_id IS NOT NULL)::int = 1
|
||||
)
|
||||
);
|
||||
COMMENT ON TABLE signature_envelopes IS
|
||||
'Pacote de assinatura — conjunto de documentos + signatários passando por uma máquina de estados única (eden.md §1.4, §5.1). envelope_number formato SIG-{ano}-{seq6digitos}, gerado pela camada de repositório (mesmo padrão de contracts.contract_number).';
|
||||
COMMENT ON COLUMN signature_envelopes.company_id IS
|
||||
'Empresa emissora (contratada) — resolve o signatário party=contratada automático a partir de companies.legal_rep_{name,cpf,email,role}. Obrigatório (NOT NULL, diferente de contracts.company_id que é opcional): createEnvelope recusa criar o envelope se essa empresa não tiver representante legal cadastrado, eden.md §1.5/§5.4.';
|
||||
COMMENT ON COLUMN signature_envelopes.status IS
|
||||
'Máquina de estados centralizada no backend, nunca confiada ao frontend — ver ALLOWED_TRANSITIONS em apps/api/src/signatures/state-machine.ts. SENT->SIGNED é um salto direto deliberado: o progresso fino (VIEWED..OTP_VERIFIED) é rastreado por signatário (signature_signers.status), não replicado passo a passo no envelope.';
|
||||
COMMENT ON COLUMN signature_envelopes.verification_id IS
|
||||
'16 bytes aleatórios em hex (crypto.randomBytes(16)), gerado só na finalização (sealEvidenceCheckpoint) — usado na URL pública de verificação /verify/{verificationId}.';
|
||||
COMMENT ON COLUMN signature_envelopes.evidence_checkpoint_hash IS
|
||||
'event_hash do último evento de auditoria do envelope no momento do selamento (sealEvidenceCheckpoint) — NÃO é o hash final absoluto da auditoria inteira, a cadeia continua recebendo eventos depois (ex. CERTIFICATE_GENERATED).';
|
||||
COMMENT ON COLUMN signature_envelopes.envelope_hash IS
|
||||
'SHA-256 dos fatos lógicos do envelope (envelope_number, evidence_checkpoint_hash, documentos e signatários ordenados por sort_order) — terceira camada de integridade, independente do PDF em bytes. Ver computeEnvelopeHash em apps/api/src/signatures/finalize.service.ts.';
|
||||
|
||||
CREATE TRIGGER trg_signature_envelopes_updated_at BEFORE UPDATE ON signature_envelopes
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE INDEX idx_signature_envelopes_status ON signature_envelopes(status);
|
||||
CREATE INDEX idx_signature_envelopes_client_registration ON signature_envelopes(client_registration_id);
|
||||
CREATE INDEX idx_signature_envelopes_reseller ON signature_envelopes(reseller_id);
|
||||
|
||||
ALTER SEQUENCE signature_envelope_seq OWNED BY NONE;
|
||||
|
||||
-- =====================================================================
|
||||
-- signature_signers — snapshot, nunca referência viva (eden.md §1.5)
|
||||
-- =====================================================================
|
||||
CREATE TABLE signature_signers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
envelope_id UUID NOT NULL REFERENCES signature_envelopes(id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
party TEXT NOT NULL DEFAULT 'contratante' CHECK (party IN ('contratada', 'contratante')),
|
||||
person_type TEXT CHECK (person_type IS NULL OR person_type IN ('pf', 'pj')),
|
||||
full_name TEXT NOT NULL,
|
||||
cpf TEXT,
|
||||
email TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
company_name TEXT,
|
||||
cnpj TEXT,
|
||||
role_title TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN (
|
||||
'PENDING', 'LINK_SENT', 'VIEWED', 'DATA_CONFIRMED', 'CONSENTED',
|
||||
'OTP_VERIFIED', 'SIGNED', 'DECLINED'
|
||||
)),
|
||||
signed_at TIMESTAMPTZ,
|
||||
secure_token_hash TEXT UNIQUE,
|
||||
secure_token_created_at TIMESTAMPTZ,
|
||||
secure_token_expires_at TIMESTAMPTZ,
|
||||
secure_token_revoked_at TIMESTAMPTZ,
|
||||
secure_token_first_used_at TIMESTAMPTZ,
|
||||
secure_token_last_used_at TIMESTAMPTZ
|
||||
);
|
||||
COMMENT ON TABLE signature_signers IS
|
||||
'Um signatário dentro de um envelope — SEMPRE um snapshot (nome/CPF/e-mail congelados no momento da criação do envelope), nunca FK viva para o cadastro do cliente. Todo envelope tem exatamente um party=''contratada'' (auto-preenchido de companies.legal_rep_*) e um ou mais party=''contratante'' (informados explicitamente na criação, eden.md §5.3).';
|
||||
COMMENT ON COLUMN signature_signers.secure_token_hash IS
|
||||
'SHA-256 simples (não HMAC) do token do link público — o "segredo" aqui é a entropia do token (32 bytes/256 bits via crypto.randomBytes), diferente do OTP (HMAC porque o espaço de 6 dígitos é pequeno). Nunca busca pelo token em claro.';
|
||||
COMMENT ON COLUMN signature_signers.status IS
|
||||
'PENDING -> LINK_SENT -> VIEWED -> DATA_CONFIRMED -> CONSENTED -> OTP_VERIFIED -> SIGNED (ou DECLINED a qualquer momento — reservado, sem rota que o produza ainda, igual ao legado). Toda escrita usa guarda AND status NOT IN (...) para nunca regredir um signatário já avançado.';
|
||||
|
||||
CREATE INDEX idx_signature_signers_envelope ON signature_signers(envelope_id);
|
||||
|
||||
-- =====================================================================
|
||||
-- signature_documents — documento congelado (cópia imutável)
|
||||
-- =====================================================================
|
||||
CREATE TABLE signature_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
envelope_id UUID NOT NULL REFERENCES signature_envelopes(id) ON DELETE CASCADE,
|
||||
source_generation_id UUID REFERENCES document_generations(id),
|
||||
document_type TEXT,
|
||||
title TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
document_version INTEGER NOT NULL DEFAULT 1,
|
||||
source_filename TEXT,
|
||||
source_mime_type TEXT,
|
||||
source_s3_key TEXT NOT NULL,
|
||||
original_sha256 TEXT NOT NULL,
|
||||
page_count INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'FROZEN' CHECK (status IN ('FROZEN', 'SUPERSEDED')),
|
||||
superseded_by_id UUID REFERENCES signature_documents(id),
|
||||
frozen_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
COMMENT ON TABLE signature_documents IS
|
||||
'Cópia congelada e imutável de um documento dentro do envelope — source_s3_key é SEMPRE uma cópia própria (nunca o objeto S3 mutável de document_generations), original_sha256 calculado no momento do congelamento. source_generation_id é só referência histórica, nunca usado operacionalmente depois de congelado (eden.md §1.6, mesma filosofia do source_attachment_id do legado).';
|
||||
|
||||
CREATE INDEX idx_signature_documents_envelope ON signature_documents(envelope_id);
|
||||
|
||||
-- =====================================================================
|
||||
-- signature_otp_challenges — ver eden.md §6 para o algoritmo completo
|
||||
-- =====================================================================
|
||||
CREATE TABLE signature_otp_challenges (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
envelope_id UUID NOT NULL REFERENCES signature_envelopes(id) ON DELETE CASCADE,
|
||||
signer_id UUID NOT NULL REFERENCES signature_signers(id) ON DELETE CASCADE,
|
||||
channel TEXT NOT NULL DEFAULT 'EMAIL',
|
||||
email_snapshot TEXT,
|
||||
otp_protected_value TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
sent_at TIMESTAMPTZ,
|
||||
verified_at TIMESTAMPTZ,
|
||||
invalidated_at TIMESTAMPTZ,
|
||||
attempts_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 5,
|
||||
request_ip INET,
|
||||
verification_ip INET,
|
||||
request_user_agent TEXT,
|
||||
verification_user_agent TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'CREATED' CHECK (status IN (
|
||||
'CREATED', 'SENT', 'VERIFIED', 'EXPIRED', 'INVALIDATED', 'BLOCKED'
|
||||
))
|
||||
);
|
||||
COMMENT ON TABLE signature_otp_challenges IS
|
||||
'otp_protected_value é SEMPRE HMAC-SHA256(código, SIGNATURE_OTP_SECRET) — o código de 6 dígitos puro nunca é gravado, logado, ou devolvido em resposta de API. TTL de 5 minutos fixo (não configurável). Um novo OTP invalida (status=INVALIDATED) qualquer challenge CREATED/SENT anterior do mesmo signatário antes de criar outro — eden.md §6.5.';
|
||||
COMMENT ON COLUMN signature_otp_challenges.max_attempts IS
|
||||
'Default 5 — ao atingir attempts_count >= max_attempts, status vira BLOCKED e nenhuma tentativa adicional é aceita nesse challenge; o signatário precisa pedir um novo OTP.';
|
||||
|
||||
CREATE INDEX idx_signature_otp_challenges_signer ON signature_otp_challenges(signer_id, created_at DESC);
|
||||
|
||||
-- =====================================================================
|
||||
-- signature_audit_events — cadeia de auditoria hash-chained, append-only
|
||||
-- =====================================================================
|
||||
CREATE TABLE signature_audit_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
seq BIGSERIAL NOT NULL UNIQUE,
|
||||
envelope_id UUID NOT NULL REFERENCES signature_envelopes(id) ON DELETE CASCADE,
|
||||
document_id UUID REFERENCES signature_documents(id),
|
||||
signer_id UUID REFERENCES signature_signers(id),
|
||||
event_type TEXT NOT NULL,
|
||||
actor_type TEXT NOT NULL CHECK (actor_type IN (
|
||||
'SIGNER', 'BACKOFFICE', 'ADMIN', 'SUPER_ADMIN', 'SYSTEM', 'SERVICE'
|
||||
)),
|
||||
actor_id UUID REFERENCES users(id),
|
||||
actor_role TEXT,
|
||||
timestamp_utc TIMESTAMPTZ NOT NULL,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
browser TEXT,
|
||||
operating_system TEXT,
|
||||
device_type TEXT,
|
||||
session_id TEXT,
|
||||
correlation_id TEXT,
|
||||
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
previous_event_hash TEXT,
|
||||
event_hash TEXT NOT NULL,
|
||||
hash_algorithm_version INTEGER NOT NULL DEFAULT 2
|
||||
);
|
||||
COMMENT ON TABLE signature_audit_events IS
|
||||
'Cadeia de auditoria hash-chained POR ENVELOPE (distinta e complementar ao audit_log genérico do sistema, que cobre toda ação crítica — as duas coexistem deliberadamente, eden.md §1.8). seq (bigserial), não timestamp_utc, define a ordem real: dentro de uma mesma transação o Postgres congela now(), então vários eventos gravados juntos podem ter timestamp idêntico. event_hash = SHA256(previous_event_hash + canonical_json({envelopeId,documentId,signerId,eventType,actorType,actorId,actorRole,timestampUtc,ipAddress,userAgent,metadata})) — note que browser/operatingSystem/deviceType/sessionId/correlationId ficam gravados na linha mas NÃO entram no hash. previous_event_hash encadeia com o evento anterior DO MESMO envelope, string vazia se for o primeiro.';
|
||||
COMMENT ON COLUMN signature_audit_events.hash_algorithm_version IS
|
||||
'Sempre 2 neste sistema (não existe v1 aqui — ver doc-comment de packages/database/src/hash-chain.ts). Mantido para o mesmo padrão defensivo do legado: se um dia a fórmula precisar mudar de novo, a verificação continua sabendo qual fórmula cada evento antigo usou.';
|
||||
|
||||
CREATE INDEX idx_signature_audit_events_envelope ON signature_audit_events(envelope_id, seq);
|
||||
|
||||
-- Append-only, reforçado em nível de banco — mesmo padrão e mesma GUC
|
||||
-- (eden.allow_audit_mutation) já usada por audit_log
|
||||
-- (1788430575858_baseline-schema.cjs), reaproveitada aqui de propósito:
|
||||
-- é a mesma classe de escotilha administrativa deliberada, não uma
|
||||
-- flag nova por tabela. Bloqueia inclusive super_admin via SQL bruto.
|
||||
CREATE OR REPLACE FUNCTION prevent_signature_audit_mutation() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF current_setting('eden.allow_audit_mutation', true) IS DISTINCT FROM 'true' THEN
|
||||
RAISE EXCEPTION 'signature_audit_events é append-only — mutação bloqueada (ver docs/security/threat-model.md)';
|
||||
END IF;
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_signature_audit_events_no_update BEFORE UPDATE ON signature_audit_events
|
||||
FOR EACH ROW EXECUTE FUNCTION prevent_signature_audit_mutation();
|
||||
CREATE TRIGGER trg_signature_audit_events_no_delete BEFORE DELETE ON signature_audit_events
|
||||
FOR EACH ROW EXECUTE FUNCTION prevent_signature_audit_mutation();
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = (pgm) => {
|
||||
pgm.sql(`
|
||||
DROP TABLE IF EXISTS signature_audit_events;
|
||||
DROP FUNCTION IF EXISTS prevent_signature_audit_mutation();
|
||||
DROP TABLE IF EXISTS signature_otp_challenges;
|
||||
DROP TABLE IF EXISTS signature_documents;
|
||||
DROP TABLE IF EXISTS signature_signers;
|
||||
DROP TABLE IF EXISTS signature_envelopes;
|
||||
DROP SEQUENCE IF EXISTS signature_envelope_seq;
|
||||
`);
|
||||
};
|
||||
Reference in New Issue
Block a user