Koder Hub — slug naming and immutability
Slugs publicados no Koder Hub são **imutáveis após o primeiro publish**. Renomear um produto se faz via `khub rename`, que cria um redirect permanente (301) do slug antigo para o novo. Slugs retirados **nunca** são recicláveis por outros produtos. O modelo é o do npm, crates.io e Apple Bundle ID — depois que um nome é usado, ele fica reservado pra sempre àquele app, mesmo após rename.
Koder Hub — slug naming and immutability
Why
A registry sem proteção de naming é vulnerável a *dentity hijack*
- App P1 publica como
slug-N. Usuários instalam, KoderUpdater pinha/apps/slug-Nem cada client. - Owner renomeia P1 →
slug-Mvia UPDATE direto.slug-Nfica livre. - App P2 (atacante OU outro produto da equipe) registra
slug-N. - KoderUpdater dos clients antigos consulta
/apps/slug-N, recebe aversão de P2, *ilently*baixa e instala (default-on per
koder-app/behaviors.kmd §4).
Resultado: supply-chain RCE em todos os instaladores antigos. O incidente left-pad de 2016 (npm) ensinou que delete/recycle de nomes de pacote publicados é uma classe de risco autocontida — autor pode quebrar cadeias em produção globalmente sem aviso.
Regras
R1 — Imutabilidade do slug após primeiro publish
Depois que um slug aparece num POST /api/v1/publish/<slug> que cria a linha em apps, ele se torna *mutável*para aquele app. UPDATE direto na coluna slug é proibido fora do dev-phase bypass; rename canônico é via khub rename / PATCH /api/v1/publish/<slug>/rename.
R2 — Reservation append-only
Toda transição de slug grava uma linha em slug_reservations (PK = slug, FK = app_id). Essa tabela é *ppend-only* produção nunca deleta linhas dela. O devphase bypass (ver `devphasedestructiveallowed.kmd`) é a única exceção.
R3 — Pre-flight de publish contra reservations
POST /api/v1/publish/<slug> faz lookup em apps.slug E em slug_reservations.slug:
- Match em
apps→ app existe, prossegue. - Match em
reservationscomapp_id= app que tenta publicar → OK(mesmo dono, slug velho).
- Match em
reservationscomapp_id≠ app que tenta publicar → *03Forbidden*"slug retired by another product".
- Sem match em nenhum lugar → 404 (app desconhecido).
R4 — Redirect permanente em GETs públicos
GET /api/v1/apps/<slug> e GET /api/v1/apps/<slug>/versions resolvem em três passos:
- Match em
apps.slug→ 200 + payload normal. - Sem match em
apps, mas match emslug_reservations→*01 Moved Permanently*para
/api/v1/apps/<slug-atual-do-app>+ headerLocation. - Nada em lugar nenhum → 404.
KoderUpdater (HTTP client dart:io / Go net/http) segue 301 automaticamente. UI deve mostrar banner "this product moved to new" na primeira vez que um redirect for detectado durante auto-update.
R5 — Histórico no AppRow
apps.previous_slugs (TEXT) preserva os slugs antigos do app, mais recente primeiro. Usado para exibir histórico de marca/rebrand em devtools.
Backfill
Slugs históricos conhecidos da Koder Stack que *evem*ser reservados mesmo retroativamente:
| Slug antigo | App atual | Razão |
|---|---|---|
kode |
koda |
Lang renomeada 2026 |
koder-store |
koder-hub |
Store → Hub rename 2026 |
kora |
kode |
IA rebrand 2026 |
Backfill é responsabilidade do operador (SQL ou script khub
backfill-reservations). A policy é satisfeita quando todos os slugs aposentados conhecidos têm linha em slug_reservations.
Out of scope
- Federação de reservations entre instâncias Hub (single-tenant hoje).
- UI rica de "este produto se mudou" — texto simples no v1.
- Migração automática de pacotes instalados — auto-update segue
redirect e instala versão atual do produto novo (intencional).
Exceção: fase pré-launch
Enquanto a Stack é puramente interna (único usuário = equipe Koder), o bypass dev-phase-destructive-allowed.kmd permite drop real (via KODER_HUB_DEV_DESTRUCTIVE=1) tanto de apps quanto de reservations. Quando a primeira release pública for cortada, o bypass some e R1–R5 viram non-negotiable.
Referências
- npm package name policy:
- crates.io name reservation: imutável após primeiro publish.
- Apple Bundle ID: imutável por toda vida do produto.
- Companheiro:
policies/dev-phase-destructive-allowed.kmd—o que esse bypass permite e quando expira.
- Implementação: ticket
products/dev/hub/backlog/done/107-slug-immutability-rename-redirect.md.