Principais pontos
- Mantenha o Auth Secret da Transloadit em um módulo
server-onlye retorne apenas opções de Assembly assinadas e de curta duração. - Autorize e limite a taxa de requisições do Route Handler de assinatura; possuir uma URL da aplicação não é permissão de upload.
- Bloqueie o Template salvo, exija assinaturas e repita as restrições de arquivo do lado do cliente em uma fronteira de processamento confiável.
Um upload seguro pelo navegador exige mais do que mover o Auth Secret para uma rota de API. O servidor precisa autorizar cada solicitação de assinatura, o payload assinado precisa indicar um Template restrito, o receptor precisa aplicar a política de arquivos e a aplicação precisa reconciliar o processamento depois que o usuário sai da página. Este design com App Router torna cada responsabilidade explícita.
O que mais importa
- Crie uma única instância do Uppy para o componente cliente e deixe que o plugin Transloadit, mantido ativamente, coordene a transferência retomável.
- Trate o progresso no navegador como progresso de transferência e use notificações verificadas para confirmar de forma durável a conclusão do processamento.
- Guarde o acesso ao armazenamento de terceiros em Credenciais de Template com privilégio mínimo, em vez de campos de requisição ou variáveis de ambiente do cliente.
Separe a autorização da aplicação da execução do upload
O navegador é um chamador não confiável, mesmo quando a interface faz parte da sua aplicação Next.js. Um Client Component pode conter a Auth Key pública e um Template ID, mas nunca deve conter o Auth Secret do Workspace, credenciais brutas de armazenamento nem autoridade para escolher Steps de processamento arbitrários. Coloque o Auth Secret atrás de um Route Handler e faça esse endpoint decidir se o usuário atual da aplicação pode iniciar exatamente este upload.
Essa decisão é separada da integridade das requisições na Transloadit. Uma assinatura válida prova que seu servidor aprovou os parâmetros serializados da Assembly por um período limitado; ela não prova que a pessoa que pede uma assinatura ao seu servidor é dona de um projeto, continua dentro da cota, passou por uma verificação de CSRF ou pode publicar o resultado. O adaptador de autorização da aplicação precisa aplicar essas regras antes de assinar. Depois, o Template salvo restringe o que a Transloadit vai executar.
Next.js
O código da aplicação autentica a sessão, verifica a propriedade dos recursos e a política de CSRF, aplica limites de taxa de requisições ou de cota e emite opções assinadas para uma finalidade específica.
Uppy
É responsável pela seleção de arquivos, pela interface de upload acessível, pelas restrições no navegador, pelo progresso, pelas novas tentativas e pela orquestração da transferência retomável.
Transloadit
Cria a Assembly, aplica a requisição assinada e o Template salvo, inspeciona os arquivos, executa o processamento e exporta os resultados configurados.
Bloqueie o Template antes de expor o formulário de upload
Crie o fluxo de trabalho de processamento como um Template salvo. Defina allow_steps_override como false para que um navegador não consiga enviar Steps substitutos junto com seu template_id. Ative “Exigir uma assinatura válida” neste Template ou exija assinaturas corretas para todo o Workspace. São controles distintos: o Template bloqueado fixa o grafo de processamento, enquanto a exigência de assinatura rejeita parâmetros assinados alterados, expirados ou inválidos por qualquer outro motivo. Sua aplicação ainda decide qual usuário pode receber esses parâmetros.
Repita as restrições de baixo custo da interface na fronteira de processamento. O exemplo limita cada arquivo a 50 MiB, limita uma Assembly a cinco arquivos e usa /file/filter com base nos metadados MIME detectados, em vez de confiar no nome do arquivo ou na declaração do navegador. Adapte a lista de permissões exata ao produto. Adicione verificação, varredura de malware, quarentena ou revisão humana quando o risco do conteúdo exigir; nenhuma verificação de MIME, sozinha, torna seguro um conteúdo arbitrário enviado por usuários.
O valor user_uploads identifica as credenciais de Template armazenadas no Workspace. Conceda a esse principal externo apenas as operações de armazenamento, o bucket e os caminhos exigidos por este fluxo de trabalho. O navegador não recebe nem assina a chave de acesso subjacente. A rotação de credenciais pode então acontecer independentemente do bundle do Next.js, mas precisa ser coordenada, porque todo Template que referencia o registro é afetado.
{
"allow_steps_override": false,
"auth": {
"max_number_of_files": 5,
"max_size": 52428800
},
"notify_url": "https://app.example.com/api/transloadit-notifications",
"steps": {
":original": {
"robot": "/upload/handle"
},
"accepted_images": {
"use": ":original",
"robot": "/file/filter",
"accepts": [
["${file.mime}", "regex", "^(image/jpeg|image/png|image/webp)$"]
],
"error_on_decline": true,
"error_msg": "Only JPEG, PNG, and WebP images are accepted"
},
"stored": {
"use": "accepted_images",
"robot": "/s3/store",
"credentials": "user_uploads"
}
}
}Assine uma única requisição de curta duração em um módulo server-only
Mantenha a configuração do servidor em variáveis de ambiente sem o prefixo NEXT_PUBLIC_ e importe server-only no topo do módulo de assinatura. O Next.js faz o build falhar se algum código do cliente importar esse módulo. O marcador é uma proteção útil, não um gerenciador de segredos: políticas de acesso em produção, ocultação de dados sensíveis nos logs, isolamento dos ambientes de preview e rotação de credenciais continuam importantes.
O Node SDK adiciona a Auth Key configurada, serializa os parâmetros e retorna exatamente essa string params com a respectiva assinatura. Retorne ambos os valores sem alterações. Analisar a string, adicionar um campo ou serializá-la novamente em outra ordem depois da assinatura produz um payload diferente, que deverá ser rejeitado. O exemplo define uma expiração de cinco minutos, já que o Uppy solicita as opções imediatamente antes da criação da Assembly, e também adiciona um nonce novo a cada autorização.
Selecione o Template no servidor. Não aceite template_id, steps, notify_url, credenciais de exportação nem valores de transformação ilimitados vindos do corpo da requisição para assiná-los às cegas. Se o produto realmente oferecer vários fluxos de upload, mapeie uma pequena operação no nível da aplicação, como avatar ou product-gallery, para um Template e limites definidos em uma allowlist, depois de verificar a permissão do usuário.
import 'server-only'
import { randomUUID } from 'node:crypto'
import { Transloadit } from 'transloadit'
export interface AssemblyOptions {
params: string
signature: string
}
type TransloaditEnvironmentName =
| 'TRANSLOADIT_KEY'
| 'TRANSLOADIT_SECRET'
| 'TRANSLOADIT_TEMPLATE_ID'
function readServerEnvironment(name: TransloaditEnvironmentName): string {
const value = process.env[name]
if (value == null || value === '') {
throw new Error('Missing Transloadit server configuration')
}
return value
}
const templateId = readServerEnvironment('TRANSLOADIT_TEMPLATE_ID')
const transloadit = new Transloadit({
authKey: readServerEnvironment('TRANSLOADIT_KEY'),
authSecret: readServerEnvironment('TRANSLOADIT_SECRET'),
})
export function createAssemblyOptions(): AssemblyOptions {
const requestParameters = {
auth: {
expires: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
nonce: randomUUID(),
},
template_id: templateId,
}
return transloadit.calcSignature(requestParameters)
}Proteja o endpoint de assinatura do App Router
Um Route Handler é acessível como qualquer outro endpoint HTTP. A função authorizeUpload do exemplo é deliberadamente específica da aplicação: conecte-a à biblioteca de sessões já existente no projeto, às verificações de propriedade de recursos, à estratégia contra CSRF e ao limitador de taxa compartilhado. Retorne a negação antes de gerar a assinatura. Em aplicações multi-tenant, defina a chave dos limites combinando tenant e usuário, e verifique o tenant que será dono do ativo resultante.
A rota não aceita parâmetros arbitrários de Assembly e marca a resposta bem-sucedida com no-store. A falha pública dela é intencionalmente genérica. Registre no servidor um identificador interno de requisição ou de trace, a categoria da decisão e o ID do ator, mas não retorne ao navegador stack traces, identificadores de conta, corpos de resposta de terceiros nem detalhes de credenciais. Deixe que falhas inesperadas passem pelo tratamento de erros centralizado e sanitizado da aplicação, em vez de envolver cada chamada em um bloco catch ruidoso.
Limitar a taxa da rota de assinatura controla a criação de Assemblies, mas não substitui os limites de cobrança da conta, os limites de arquivos do Template nem as cotas da aplicação. Aplique os três. Uma assinatura é uma capacidade de curta duração: qualquer pessoa que obtenha o payload assinado completo pode tentar enviá-lo enquanto ele continuar válido. Por isso, transmita-o apenas por HTTPS e evite analytics, armazenamento no navegador, URLs e logs que o retenham.
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { authorizeUpload } from '../../../server/upload-authorization'
import {
type AssemblyOptions,
createAssemblyOptions,
} from '../../../server/transloadit-options'
interface ErrorResponse {
error: string
}
export async function POST(
request: NextRequest,
): Promise<NextResponse<AssemblyOptions | ErrorResponse>> {
const permission = await authorizeUpload(request)
if (!permission.allowed) {
return NextResponse.json({ error: 'Upload not allowed' }, { status: 403 })
}
return NextResponse.json(createAssemblyOptions(), {
headers: { 'Cache-Control': 'no-store' },
})
}Autentique
Resolva uma sessão nova no servidor em vez de confiar em um identificador de usuário ou de tenant fornecido pelo cliente.
Autorize
Verifique se o ator pode fazer upload para o recurso e a operação de destino antes de criar a capacidade assinada.
Limite
Imponha limites de taxa por usuário e por tenant, limites de trabalho simultâneo, a política de armazenamento e as cotas de negócio, além dos limites do Template.
Monte o Uppy uma única vez dentro de um Client Component
O Uppy precisa de APIs do navegador, então o uploader fica atrás de uma fronteira use client. Crie a instância do Uppy uma única vez com um inicializador lazy de estado. Recriá-la durante a renderização descarta os arquivos selecionados e quebra a responsabilidade pelo ciclo de vida. Este componente destrói a instância quando é dono de todo o tempo de vida do upload; se os uploads precisarem sobreviver à navegação entre rotas, eleve a instância para um provider de cliente de vida mais longa e destrua-a quando esse provider for encerrado.
A função assíncrona assemblyOptions chama a rota protegida enquanto o Uppy prepara o upload. Ela verifica response.ok, trata com segurança JSON malformado, valida o formato da resposta e expõe apenas um erro de autorização estável voltado ao usuário. O Dashboard do Uppy fornece a interface de seleção, progresso, cancelamento e erros, enquanto o plugin da Transloadit cria a Assembly e envia os arquivos por uma infraestrutura de upload retomável. A programação limitada de novas tentativas ajuda em falhas temporárias sem repetir indefinidamente.
Essas novas tentativas funcionam enquanto esta instância do Uppy permanece ativa. Elas não fazem o componente mostrado recuperar os arquivos selecionados e o estado da Assembly depois de um recarregamento da página. Se a recuperação após recarregamento for um requisito do produto, configure e teste o Golden Retriever ou outro design de persistência documentado com o plugin da Transloadit; não presuma essa capacidade apenas com base no tus ou em retryDelays.
Os limites do navegador espelham o Template para dar feedback rápido, mas não são a fronteira de segurança. Quem faz a chamada pode contornar o componente, e os metadados do arquivo podem ser falsos. Mantenha as regras autoritativas no Template bloqueado: contagem de arquivos, tamanho em bytes, verificações do conteúdo detectado e política de exportação. Decida também se o cancelamento no navegador deve cancelar apenas a transferência, a Assembly ou o registro do ativo na aplicação e, depois, teste essa decisão em vez de presumir que os três estados são idênticos.
'use client'
import Uppy from '@uppy/core'
import Dashboard from '@uppy/react/dashboard'
import Transloadit from '@uppy/transloadit'
import { type ReactNode, useEffect, useState } from 'react'
import { z } from 'zod'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
const assemblyOptionsSchema = z.object({
params: z.string().min(1),
signature: z.string().regex(/^(sha1|sha256|sha384):[0-9a-f]+$/),
})
async function fetchAssemblyOptions(): Promise<z.infer<typeof assemblyOptionsSchema>> {
const response = await fetch('/api/transloadit-params', {
method: 'POST',
headers: { Accept: 'application/json' },
})
if (!response.ok) {
throw new Error('Could not authorize this upload. Try again.')
}
const responseBody: unknown = await response.json().catch(() => null)
const parsedOptions = assemblyOptionsSchema.safeParse(responseBody)
if (!parsedOptions.success) {
throw new Error('Could not authorize this upload. Try again.')
}
return parsedOptions.data
}
function createUppy(): Uppy {
return new Uppy({
restrictions: {
allowedFileTypes: ['image/jpeg', 'image/png', 'image/webp'],
maxFileSize: 50 * 1024 * 1024,
maxNumberOfFiles: 5,
},
}).use(Transloadit, {
assemblyOptions: fetchAssemblyOptions,
retryDelays: [0, 1_000, 3_000, 5_000],
waitForEncoding: false,
})
}
export function UploadForm(): ReactNode {
const [uppy] = useState(createUppy)
useEffect(() => {
return () => uppy.destroy()
}, [uppy])
return <Dashboard height={420} proudlyDisplayPoweredByUppy={false} uppy={uppy} />
}Reconcilie o progresso, as novas tentativas e a conclusão
O progresso da transferência e o progresso do processamento respondem a perguntas diferentes. Com waitForEncoding: false, a interface pode terminar depois que os bytes chegam à Transloadit, enquanto a validação, a transformação e a exportação continuam. Escute transloadit:assembly-created e associe o Assembly ID a um registro pendente da aplicação. Assim, uma navegação pode interromper a observação no navegador sem perder a identidade necessária para a reconciliação.
Defina um notify_url fixo no Template controlado pelo servidor para a conclusão assíncrona. O handler de notificações deve verificar a assinatura usando o Auth Secret associado à Auth Key da Assembly, validar o payload, associar o Assembly ID ao registro pendente esperado e aplicar os resultados de forma idempotente antes de retornar sucesso. As notificações podem ser reenviadas, então uma duplicata deve confirmar o estado terminal existente em vez de criar outro ativo ou evento de publicação.
Use waitForEncoding: true apenas em fluxos de trabalho curtos, nos quais o usuário deve permanecer na página e o código do navegador realmente precisa dos resultados finais. Mesmo assim, mantenha um caminho de recuperação no lado do servidor, porque abas são fechadas e conexões caem. Uma tarefa agendada de reconciliação pode consultar Assemblies não terminais cujas notificações se perderam. Separe a política de novas tentativas por tipo de falha: retome após uma interrupção de rede, solicite novas opções assinadas quando elas expirarem antes da criação da Assembly e pare diante de uma rejeição de política até que o usuário troque o arquivo.
Selecionado
O navegador tem um arquivo candidato; nenhum sistema confiável o aceitou ainda.
Upload concluído
O receptor tem os bytes, mas a validação autoritativa, o processamento ou a exportação ainda podem falhar.
Pronto
Um resultado terminal verificado foi persistido e está autorizado para o uso pretendido na aplicação.
Teste os controles como um atacante e como um usuário interrompido
Teste o endpoint de assinatura sem sessão, com o tenant errado, com um token CSRF ausente ou inválido quando aplicável, acima do limite de taxa e após a revogação de permissões. Confirme que nenhuma resposta ou log contém o Auth Secret, as Credenciais de Template, um stack trace ou um erro bruto de dependência. Tente substituir o template_id, adicionar steps, estender auth.expires e enviar opções assinadas depois de expiradas; a aplicação da assinatura ou do Template deve rejeitar a solicitação inválida.
Teste os seguintes casos: um JPEG permitido, um tipo MIME não permitido com extensão de imagem, um arquivo grande demais, arquivos em excesso, um arquivo de zero bytes, uma perda de conexão em vários offsets, o recarregamento do navegador, o cancelamento, uma autorização expirada, uma notificação duplicada, uma negação de armazenamento e uma falha de processamento após o upload. Confirme que o comportamento ao recarregar corresponde ao design de persistência, em vez de presumir uma recuperação automática. Verifique se o progresso e os anúncios de erro são acessíveis com teclado e tecnologia assistiva e, em seguida, inspecione o armazenamento temporário e os registros da aplicação em busca de vazamentos ou de estados pendentes permanentes.
Monitore negações de assinatura, decisões de limite de taxa, taxas de criação e de falha de Assemblies, recuperação de uploads, latência de processamento, idade das notificações, erros de armazenamento e idade dos registros pendentes, sem registrar payloads protegidos em log. Crie alertas para mudanças persistentes, e não para erros individuais de usuários. Mantenha o Template ID e uma versão do fluxo de trabalho gerenciada pela aplicação nos registros operacionais, porque um Template salvo pode mudar com o tempo.
Detalhes técnicos que vale a pena conhecer
- Um arquivo
route.tsdo App Router é um endpoint HTTP, então ele precisa realizar sua própria autenticação, autorização, controles contra abuso e validação de entrada antes de retornar uma assinatura. - O marcador de pacote
server-onlygera um erro em tempo de build se um módulo protegido for importado em um Client Component, mas os segredos de implantação ainda precisam de configuração correta na plataforma e de controles de acesso. - O método
calcSignaturedo Node SDK da Transloadit adiciona a Auth Key quando configurada, serializa os parâmetros da solicitação e retorna exatamente essa stringparamscom sua assinatura HMAC. - A Signature Authentication cobre
auth.expirese o restante do payload serializado da solicitação; alterar um valor protegido depois da assinatura invalida a assinatura. - Um Template pode aceitar, por padrão, substituições de Steps em tempo de execução; por isso, fluxos de trabalho controlados pelo navegador devem definir
allow_steps_overridecomofalse, a menos que uma substituição específica e cuidadosamente revisada seja intencional. - As restrições do Uppy oferecem feedback imediato no navegador, enquanto
auth.max_sizedo Template,auth.max_number_of_filese os Steps de processamento de arquivos aplicam a política depois que o código do navegador já não pode ser considerado confiável. - O plugin da Transloadit aceita uma função assíncrona
assemblyOptions, cria uma Assembly e configura uploads retomáveis para o endpoint tus da Assembly. - Com
waitForEncoding: false, o Uppy conclui seu trabalho após a transferência, e não após o processamento; a aplicação deve guardar o Assembly ID e usar uma notificação com assinatura verificada ou uma consulta de status posterior para obter o resultado terminal. - O exemplo com a configuração limitada de
retryDelaysretoma a transferência após falhas transitórias enquanto sua instância do Uppy permanece ativa; a recuperação após um recarregamento exige estado persistido do Uppy e da Transloadit, como uma integração do Golden Retriever configurada deliberadamente. - As Credenciais de Template são registros mantidos no Workspace e referenciados por nome, então segredos brutos de armazenamento não aparecem no JSON do Template salvo, no bundle do cliente nem nos parâmetros assinados da Assembly; o Template contém apenas o nome do registro de credencial.
Uma abordagem prática
- 1
Crie um Template bloqueado que exija assinatura, com limites de upload, verificações do conteúdo detectado e Credenciais de Template com escopo restrito.
- 2
Adicione um helper de assinatura exclusivo do servidor e um endpoint do App Router autenticado, com limite de taxa de requisições e não armazenável em cache.
- 3
Monte uma única instância do Uppy em um Client Component com restrições alinhadas, novas tentativas e falhas de autorização sanitizadas.
- 4
Persista o Assembly ID, verifique as notificações de conclusão e teste recusa, interrupção, expiração, repetição e entrega duplicada.
Quando a Transloadit é útil
Use o plugin Transloadit do Uppy, mantido ativamente, quando um fluxo de navegador no Next.js precisar de uploads retomáveis seguidos de validação, transformação e exportação gerenciadas. Um Template salvo fixa o fluxo de trabalho permitido, a Signature Authentication protege os parâmetros de requisição aprovados e o prazo de expiração, e as Credenciais de Template mantêm os segredos brutos de armazenamento fora tanto dos bundles de cliente do Next.js quanto dos parâmetros da Assembly.
Limite da arquitetura
Sua aplicação Next.js autentica o usuário e decide se emite uma autorização de upload de curta duração. O Uppy é responsável pela seleção no navegador, pelo progresso e pela orquestração da transferência. A Transloadit recebe os bytes, aplica o Template de validação e processamento configurado e informa os resultados. Nenhuma dessas camadas substitui o registro durável do ativo na aplicação nem sua política de publicação.
Perguntas frequentes
Um Client Component do Next.js pode conter a Auth Key da Transloadit?
A Auth Key identifica o Workspace e pode aparecer em uma requisição assinada, mas o Auth Secret deve permanecer no servidor. Ainda assim, exija assinaturas e restrinja o Template, porque uma Auth Key exposta sem esses controles pode permitir requisições não autorizadas.
Por que usar um Route Handler em vez de assinar em um Server Component?
O Uppy solicita novas opções de Assembly a partir do código do navegador imediatamente antes do upload. Um Route Handler fornece essa fronteira HTTP, mas precisa autenticar e autorizar a requisição como qualquer outro endpoint de mutação. Uma Server Function poderia implementar uma fronteira semelhante se a integração a chamasse com segurança.
Uma requisição assinada para um Template bloqueado impede todo tipo de abuso de upload?
Não. Ela protege a integridade da requisição e restringe a receita de processamento. Você ainda precisa de autorização na aplicação, limites de taxa de requisições e de cobrança, limites de quantidade de arquivos e de bytes, validação do conteúdo detectado, armazenamento com privilégio mínimo e qualquer varredura ou revisão exigida pelo modelo de ameaças do produto.
waitForEncoding deve ser true no Next.js?
Geralmente não, para trabalhos longos. Com false, o usuário espera a transferência e a aplicação conclui o processamento por meio de uma notificação verificada. Defina como true apenas quando o fluxo de trabalho for curto e o código do navegador precisar dos resultados finais, mantendo a reconciliação no lado do servidor para abas fechadas e conexões perdidas.
Onde devem ficar as credenciais do S3 ou de outros serviços de armazenamento?
Armazene-as como Credenciais de Template da Transloadit com privilégio mínimo e referencie o registro pelo nome a partir do Template salvo. Não coloque credenciais brutas do provedor em variáveis de ambiente públicas do Next.js, no código do cliente, em campos de requisição assinada, em logs nem em metadados de resultados.
As restrições de arquivo do Uppy bastam para validar uploads?
Não. Elas melhoram o feedback para usuários cooperativos. Repita os limites de bytes e de quantidade no Template e inspecione as propriedades detectadas dos arquivos com Steps de processamento confiáveis, porque quem faz a chamada pode contornar o JavaScript do navegador e as declarações podem ser enganosas.