Skip to main content

Convite de anexo externo — API

Permite que um operador autenticado gere um convite de upload — um token opaco, de uso único e de curta duração — para que um terceiro sem login no portal (ex.: um paciente ou outra clínica) anexe documentos a um exame específico, sem precisar de uma conta no Portal 2.0. O terceiro usa esse token como um Bearer token dedicado a esta funcionalidade (uma "capability" isolada), não como sessão de usuário: as duas rotas de consumo (inspect e redeem) não passam pelo JwtAuthenticationGuard nem por AuthorizationGuard — só exigem o token do convite.

Funcionamento​

Criar convite (POST /exams/:examId/external-attachment-invitations, autenticado): confirma que o exame existe, que a unidade tem anexos externos habilitados (política de workflow por unidade) e que a unidade está ativa. Gera um token opaco aleatório (32 bytes, base64url) e grava só o hash SHA-256 do token — o valor em claro existe apenas na resposta HTTP desta chamada. O convite nasce com validade de 10 minutos, fixos no código.

Consultar convite (GET /external-attachment-invitations/current, público — só com o token): usado pela tela que o terceiro abre a partir do link. Resolve o convite pelo hash do token apresentado, confere que não expirou e que ainda não foi consumido, e revalida que o exame e a unidade do convite ainda correspondem ao que foi emitido (proteção contra o exame ter mudado de unidade/tenant entre a emissão e o uso). Devolve só o necessário para montar a tela: classificações permitidas, prazo de expiração, limites de upload e um resumo mínimo do exame (nome do paciente, nascimento, descrição do estudo, data de realização).

Consumir convite (POST /external-attachment-invitations/current/attachment-batches, público — só com o token): recebe um lote de 1 a 20 arquivos (multipart/form-data) com uma classificação por arquivo, valida tipo/tamanho/conteúdo de cada um, envia todos ao armazenamento de objetos e só então grava os anexos e marca o convite como consumido — tudo em uma única transação. Exige um header Idempotency-Key (UUID): reenviar a mesma chave num convite já consumido devolve os mesmos anexos já criados, sem duplicar; uma chave diferente num convite já consumido é rejeitada.

Endpoints​

MétodoRotaDescrição
POST/v1/exams/:examId/external-attachment-invitationsEmite um convite de upload externo (autenticado)
GET/v1/external-attachment-invitations/currentConsulta os dados públicos do convite (só com o token)
POST/v1/external-attachment-invitations/current/attachment-batchesConsome o convite e cria o lote de anexos (só com o token)

Versão: v1

Swagger:

Rota (Dev): http://localhost:3000/v1/external-attachment-invitations/current

Fluxo completo (emissão pelo operador → uso pelo terceiro externo):

Lógica de decisão de POST .../attachment-batches (consumir o convite):

Permissões​

RotaGuardsAcesso
POST /exams/:examId/external-attachment-invitationsJwtAuthenticationGuard, AuthorizationGuardUsuário autenticado com permissão exam:add-attachment no escopo do exame (RLS por examId no path)
GET /external-attachment-invitations/currentExternalAttachmentInvitationCapabilityGuardPúblico — qualquer portador do header Authorization: Bearer <token> com formato válido; a validade real do token é conferida depois, no service
POST /external-attachment-invitations/current/attachment-batchesExternalAttachmentInvitationCapabilityGuardIdêntico ao anterior

O ExternalAttachmentInvitationCapabilityGuard só confere se o header Authorization casa com o padrão Bearer <algo>; não decodifica nem valida o token — se o formato bater mas o token for inválido/expirado/inexistente, o service de cada rota lança EXTERNAL_ATTACHMENT_INVITATION_NOT_FOUND (404) ou o erro correspondente. Isso significa que estas duas rotas nunca exigem login no portal — são a única via não autenticada deste módulo, por desenho.

Headers​

HeaderObrigatórioDescrição
AuthorizationSim, nas 3 rotasPOST /exams/:examId/...: Bearer <access_token> do operador; GET/POST .../attachment-batches: Bearer <token do convite>
Idempotency-KeySim, só em POST .../attachment-batchesUUID; chave de replay do lote
Content-TypeSim em POST .../attachment-batchesmultipart/form-data
Cache-Control: no-storeEnviado pela APIPresente na resposta de GET e do POST de consumo — evita cache de dados do exame/convite

Path parameters​

NomeTipoObrigatórioDescrição
examIdUUIDSim, só em POST /exams/:examId/...Exame para o qual o convite é emitido (ParseUUIDPipe)

Query parameters​

Nenhum.

Body​

Criar convite — CreateDiagnosisExternalAttachmentInvitationRequest:

json
{ "classifications": ["PATIENT_DOCUMENT", "CONSENT_FORM"] }
CampoTipoObrigatórioValidação
classificationsarray de string (enum)Não@IsEnum(AttachmentClassification, { each: true }); se omitido ou vazio, o convite libera todas as classificações

Consumir convite — multipart/form-data:

CampoTipoObrigatórioValidação
filesarquivos (binário)Sim1 a 20 arquivos (FilesInterceptor), até 50MB cada, 200MB no total
itemsstring (JSON stringificado)SimArray com um item por arquivo, na mesma ordem/posição: { "classification": "<enum>", "position": <índice> }; posições devem ser contíguas e únicas, cobrindo todos os arquivos

Tipos de arquivo aceitos (validados por MIME e pelos primeiros bytes do conteúdo, não só pela extensão): pdf, png, jpg/jpeg, docx, xlsx, xls, csv, zip.

GET /external-attachment-invitations/current não recebe corpo.

Response​

201 — Criar convite (DiagnosisExternalAttachmentInvitationResponse):

json
{
"data": {
"classifications": ["PATIENT_DOCUMENT", "CONSENT_FORM"],
"expiresAt": "2026-09-24T12:10:00.000Z",
"token": "opaque-invitation-token-base64url"
}
}

O token só aparece nesta resposta. Ele não pode ser recuperado depois — só o hash fica salvo.

200 — Consultar convite (PublicDiagnosisExternalAttachmentInvitationResponse):

json
{
"data": {
"classifications": ["PATIENT_DOCUMENT", "CONSENT_FORM"],
"exam": {
"patientName": "Fulano de Tal",
"patientBirthDate": "1990-01-01",
"studyDescription": "Tomografia de tórax",
"performedAt": "2026-09-20T10:00:00.000Z"
},
"expiresAt": "2026-09-24T12:10:00.000Z",
"limits": { "maxFiles": 20, "maxFileBytes": 52428800, "maxTotalBytes": 209715200 }
}
}

201 — Consumir convite (RedeemedDiagnosisExternalAttachmentBatchEnvelopeResponse):

json
{
"data": {
"attachments": [
{
"id": "0196cf9a-...-uuid",
"examId": "0196cf9a-...-uuid",
"classification": "PATIENT_DOCUMENT",
"fileName": "documento.pdf",
"fileType": "application/pdf"
}
]
}
}

Erros​

RotaClasse de erroerrorCodeStatusQuando ocorre
Criar convite(guard)UNAUTHENTICATED / FORBIDDEN_ACTION401 / 403sem token válido ou sem exam:add-attachment no exame
Criar conviteDiagnosisExternalAttachmentsDisabledErrorEXTERNAL_ATTACHMENTS_DISABLED403a política de workflow da unidade não tem anexo externo habilitado
Criar conviteDiagnosisExternalAttachmentPolicyUnavailableErrorEXTERNAL_ATTACHMENT_POLICY_UNAVAILABLE503a política da unidade não pôde ser resolvida (falha de dependência) — falha fechada: nega por padrão
Criar conviteDiagnosisUnitNotFoundErrorUNIT_NOT_FOUND404unidade do exame inexistente ou inativa
GET/consumir(guard)EXTERNAL_ATTACHMENT_INVITATION_NOT_FOUND404header Authorization ausente ou fora do formato Bearer <token>
GET/consumirDiagnosisExternalAttachmentInvitationNotFoundErrorEXTERNAL_ATTACHMENT_INVITATION_NOT_FOUND404token desconhecido, ou exame/unidade/tenant do convite não batem mais com o cadastro atual
GET/consumirDiagnosisExternalAttachmentInvitationExpiredErrorEXTERNAL_ATTACHMENT_INVITATION_EXPIRED410convite expirado (10 minutos após a emissão)
GET/consumirDiagnosisExternalAttachmentInvitationAlreadyUsedErrorEXTERNAL_ATTACHMENT_INVITATION_ALREADY_USED409convite já consumido com uma Idempotency-Key diferente da apresentada
ConsumirDiagnosisExternalAttachmentClassificationNotAllowedErrorEXTERNAL_ATTACHMENT_CLASSIFICATION_NOT_ALLOWED400alguma classificação enviada não está entre as permitidas pelo convite
ConsumirDiagnosisExternalAttachmentBatchInvalidErrorEXTERNAL_ATTACHMENT_BATCH_INVALID400Idempotency-Key ausente/não-UUID; quantidade de items diferente da de arquivos; posições não contíguas; nome de arquivo inválido; tipo de arquivo não suportado ou conteúdo não bate com a extensão declarada
ConsumirDiagnosisExternalAttachmentBatchTooLargeErrorEXTERNAL_ATTACHMENT_BATCH_TOO_LARGE413mais de 20 arquivos, um arquivo acima de 50MB, ou lote acima de 200MB no total
ConsumirDiagnosisExternalAttachmentsDisabledErrorEXTERNAL_ATTACHMENTS_DISABLED403a política da unidade foi desabilitada entre a emissão e o consumo do convite
ConsumirDiagnosisExternalAttachmentPolicyUnavailableErrorEXTERNAL_ATTACHMENT_POLICY_UNAVAILABLE503falha ao resolver a política no momento do consumo
ConsumirDiagnosisExternalAttachmentUploadUnavailableErrorEXTERNAL_ATTACHMENT_UPLOAD_UNAVAILABLE503falha inesperada durante upload/gravação; os objetos já enviados ao armazenamento são removidos antes de responder

Regras de negócio​

IDRegraComportamento esperado
RN-01O token em claro só existe uma vezsó o hash SHA-256 é persistido; perder o token da resposta de criação torna o convite inutilizável para consulta manual (mas ele continua existindo até expirar)
RN-02Convite expira em 10 minutos, fixos no códigonão é configurável por variável de ambiente
RN-03Convite sem classifications libera todaslista vazia ou omitida é normalizada para todos os valores de AttachmentClassification
RN-04Convite é de uso único, mas idempotente por chavereenviar a mesma Idempotency-Key num convite já consumido devolve os mesmos anexos (sem duplicar); chave diferente é rejeitada
RN-05O escopo do convite é revalidado no consumo, não só na criaçãose o exame mudar de unidade/tenant entre a emissão e o uso, o convite passa a ser tratado como não encontrado
RN-06A política de anexo externo é checada duas vezesuma vez na criação do convite, outra de novo no consumo — pode ter mudado nesse intervalo
RN-07Anexos criados via convite externo têm origem própriagravados com origin: EXTERNAL_INVITATION (distinto de INTERNAL_OPERATOR) e sempre com isKeyImage: false e isPatientExam: false
RN-08Falha ao resolver a política de anexo externo nega por padrão (fail-closed)indisponibilidade da dependência de política vira 503, nunca libera o upload por omissão
RN-09Tipo de arquivo é validado por assinatura de bytes, não só por extensão/MIME declaradoex.: um arquivo renomeado para .pdf sem o cabeçalho %PDF- de um PDF real é rejeitado
RN-10Upload no armazenamento acontece antes da gravação em bancose a transação de gravação falhar, os objetos já enviados são removidos do armazenamento

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDminimização de dados expostos ao terceiro externoGET /external-attachment-invitations/current devolve só nome do paciente, nascimento, descrição do estudo e data — nenhum outro dado do exame ou do prontuário
HIPAAacesso de terceiro não autenticado é restrito, auditado e temporáriotoken opaco de uso único, TTL de 10 min, e eventos de auditoria diagnosis.external-attachment-invitation.issued e .redeemed com actorUserId (o emissor, mesmo no consumo pelo terceiro)
ANVISA (indireto)rastreabilidade de documentos anexados por terceiroanexos via convite carregam origin: EXTERNAL_INVITATION, distinguindo-os de upload interno na trilha de auditoria

Variáveis de ambiente​

Nenhuma variável de ambiente foi encontrada controlando TTL do convite, tamanho do token ou limites de upload — todos são constantes no código (10 min de validade, token de 32 bytes, até 20 arquivos, 50MB por arquivo, 200MB por lote).

Tempo médio de resposta​

A confirmar — responsável: time de Diagnosis; data: 24/09/2026. Não há medição publicada; não foi executado neste levantamento.

Requisitos não funcionais​

RequisitoDefinição
IdempotênciaO consumo do convite é idempotente por Idempotency-Key (replay seguro); criar convite e consultar convite não são idempotentes/não se aplicam
PaginaçãoNão se aplica
Rate limitGET /external-attachment-invitations/current: 20 requisições / 60s (@Throttle, sem balde adicional por identidade); as demais rotas deste módulo não têm @Throttle próprio e usam só o limite global padrão da API
CacheNão — Cache-Control: no-store explícito nas duas rotas do fluxo externo
AuditoriaSim — diagnosis.external-attachment-invitation.issued na criação, .redeemed por anexo no consumo

Relacionado​

  • 🖥️ Tela: A confirmar — responsável: time de frontend; data: 24/09/2026. (fora do escopo deste levantamento, que cobriu apenas o backend)
  • 📂 Módulo: Anexos (Attachment)
  • 🔗 Fluxo relacionado: Anexos do exame — via de upload para usuário autenticado do portal, sobre o mesmo exame