Koder Hub — slug naming and immutability

mandatory

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*

  1. App P1 publica como slug-N. Usuários instalam, KoderUpdater pinha

    /apps/slug-N em cada client.

  2. Owner renomeia P1 → slug-M via UPDATE direto. slug-N fica livre.
  3. App P2 (atacante OU outro produto da equipe) registra slug-N.
  4. KoderUpdater dos clients antigos consulta /apps/slug-N, recebe a

    versã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 reservations com app_id = app que tenta publicar → OK

    (mesmo dono, slug velho).

  • Match em reservations com app_id ≠ app que tenta publicar → *03

    Forbidden*"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:

  1. Match em apps.slug → 200 + payload normal.
  2. Sem match em apps, mas match em slug_reservations

    *01 Moved Permanently*para /api/v1/apps/<slug-atual-do-app> + header Location.

  3. 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 20260503 (Brand 82)
koder-store koder-hub Store → Hub rename 20260429
kora kode IA rebrand 20260503

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:

    https://docs.npmjs.com/policies/unpublish

  • 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.

Source: ../home/koder/dev/koder/meta/docs/stack/policies/registry-naming.kmd