Tipo: Azion Edge Function (TypeScript, runtime Web standard / Service Worker API). Responsabilidade: servir
GET /v1/snapshotpublicamente, atuando como camada de cache global na frente da Go API. Inspiração:dev/azion/js-azion-api-scaffold— adotamos as mesmas convenções de entry point, env bridge, middleware chain, logger e formato de erro, mas sem auth e sem database.
| Componente | Escolha | Por quê |
|---|---|---|
| Framework | Hono | Mesmo do scaffold; leve, edge-compatível, API web nativa |
| Validação | Zod | Mesmo do scaffold; valida X-Client-Id no header |
| Runtime | Azion Edge Runtime + Node em dev | Igual ao scaffold |
| Build | esbuild | Bundling pro edge; replica build:azion do scaffold |
| Deploy | Azion CLI (azion deploy) |
Decisão já firmada no PRD |
| Testes | Vitest | Padrão do scaffold |
| Erros | @azion/js-api-errors (JSON:API) |
Padrão Azion para respostas de erro |
Não usamos
@azion/js-authaqui — a Function não autentica usuários; só repassa o headerX-Client-Idrecebido. Auth não-existe-no-MVP (ver PRD §7.2).
function/
├── README.md # setup, dev, deploy
├── ARCHITECTURE.md # este documento
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
├── azion/
│ ├── azion.json # config do Azion CLI (name, preset, function id)
│ └── args.json.example # template de env vars passadas como FetchEvent.args
├── scripts/
│ └── deploy.sh # gera args.json + build + azion deploy
└── src/
├── azion.ts # LOCKED. Entry edge (addEventListener('fetch')).
├── azion.d.ts # LOCKED. Tipos do runtime Azion (FetchEvent.args).
├── env.ts # LOCKED. getEnv() — bridge args + process.env.
├── server.ts # Dev server Node (Hono via @hono/node-server).
├── index.ts # App Hono: middleware chain + rota /v1/snapshot.
├── config.ts # Config tipado: GO_API_URL, INTERNAL_TOKEN, CACHE_MAX_AGE.
├── types.ts # AppEnv + tipos compartilhados (sem AuthResult).
├── handlers/
│ ├── snapshot.ts # Handler de GET /v1/snapshot.
│ ├── snapshot.test.ts # Testa transformação + cache headers + erros.
│ └── health.ts # GET /healthz (eco simples; sem deep check).
├── middleware/
│ ├── security.ts # requestId / timeout / bodyLimit / secureHeaders.
│ ├── validation.ts # Zod validators (header / params).
│ └── client-id.ts # Lê e valida X-Client-Id; injeta em c.var.
├── clients/
│ └── api-client.ts # fetch contra a Go API (/internal/snapshot) com AbortSignal.timeout.
└── utils/
└── logger.ts # Compliance logger (clientIp, clientPort, uri, etc.).
Idêntico ao scaffold:
src/azion.tsregistraaddEventListener('fetch'), chamasetAzionArgs(event.args), e delega paraapp.fetch().src/server.tsroda o mesmoapplocalmente em Node via@hono/node-serverpra dev.src/env.tsé ogetEnv()que primeiro consulta_azionArgs(no edge) e cai praprocess.env(em dev).
src/index.ts exporta o app Hono compartilhado pelas duas entradas.
requestId → complianceLogger → timeout(30s) → bodyLimit(8KB)
→ secureHeaders → cors → [por rota] clientIdMiddleware → handler
Diferenças em relação ao scaffold:
- Sem
azionAuthMiddleware— substituído peloclientIdMiddleware. bodyLimitreduzido de 100KB pra 8KB — não recebemos body em/v1/snapshot.- CORS habilitado pra permitir browsers consumirem o snapshot.
app.get(
'/v1/snapshot',
clientIdMiddleware, // valida X-Client-Id
snapshotHandler
);export async function snapshotHandler(c: Context<AppEnv>) {
const clientId = c.get('clientId'); // injetado pelo middleware
const internal = await fetchInternalSnapshot(clientId);
if (internal.status === 404) return jsonApiError(c, 404, 'Snapshot not found');
const transformed = {
flags: mapEnabledOnly(internal.body.flags), // {key: bool}
generated_at: internal.body.generated_at,
};
return c.json(transformed, 200, {
'Cache-Control': `public, max-age=${cfg.cacheMaxAge}`,
'Content-Type': 'application/json',
});
}Segue o padrão de clients/example-client.ts do scaffold:
AbortSignal.timeout(cfg.apiTimeoutMs)em toda chamada.- Headers
X-Internal-Token(segredo compartilhado) +X-Client-Id(repassado do request). - Tipa o response:
InternalSnapshot { schema_version, client_id, generated_at, flags: Record<string, FlagFull> }.
export async function fetchInternalSnapshot(clientId: string): Promise<InternalResult> {
const config = getApiConfig();
const response = await fetch(`${config.baseUrl}/internal/snapshot`, {
method: 'GET',
headers: {
'X-Internal-Token': config.internalToken,
'X-Client-Id': clientId,
},
signal: AbortSignal.timeout(config.timeout),
});
// ...
}O cache do edge node é controlado por dois lados:
- Header
Cache-Control: public, max-age=60na resposta — informa proxies/CDNs. - Azion Cache Rules (via
azion.jsonou configuração no painel) — cacheia respostas por 30s comX-Client-Idna chave de cache (cache key vary). Isolamento por conta.
Documentar a regra de cache no README.md da function como passo manual pós-deploy (ou via JSON declarativo se a CLI suportar — verificar).
# Acesso à Go API
GO_API_URL=https://flags-api.example.com
INTERNAL_TOKEN=<segredo compartilhado com a Go API>
API_TIMEOUT_MS=5000
# Cache
CACHE_MAX_AGE=60 # segundos enviados no Cache-Control ao cliente
# Server (dev local)
PORT=3000
Em produção, populadas via azion/args.json (gerado pelo scripts/deploy.sh).
- Reusar o
complianceLoggerMiddlewaredo scaffold integralmente — boa prática mesmo sem auth. - Sanitização de headers sensíveis:
X-Internal-Tokenadicionado à lista FORBIDDEN. - Cada request loga
request.started+request.completedcomstatusCodeedurationMs. X-Client-Idé considerado identificador legítimo (não-sensível) — pode aparecer nos logs.
Mantém o padrão JSON:API do scaffold:
| Status | Caso |
|---|---|
| 400 | X-Client-Id ausente ou fora do regex |
| 404 | Snapshot não existe para esse client_id |
| 504 | Timeout chamando a Go API |
| 502 | Go API retornou erro inesperado |
| 500 | Erro interno na Function |
snapshot.test.ts— usaapp.request()com headers mockados; mockafetchInternalSnapshot; verifica:- resposta transformada correta (
{key: bool}) Cache-ControlpresenteX-Client-Idausente → 400- timeout → 504
- body 200 com
generated_atpreservado
- resposta transformada correta (
- Sem testes E2E aqui — ficam num diretório separado quando todo o stack estiver up.
- 🟡 Configurar Cache Rules via
azion.jsonou só via painel? (verificar suporte da CLI 2025/2026) - 🟡 Logar
X-Client-Idno campometadata.clientIdou só no body do request? (padrão do scaffold émetadata.clientId) - 🟡 Health check
/healthzrealmente útil aqui ou redundante com Azion's próprio? (incluído por padrão, custo zero)