TypeID - identificadores globais únicos, seguros por tipo e ordenáveis em K, inspirados no Stripe ID
(github.com/jetpack-io)- TypeID é um identificador seguro por tipo que estende o UUIDv7, um formato de identificador global único inspirado na forma como a API da Stripe usa prefixos
- A string canônica é em minúsculas e composta por três partes: um prefixo de tipo ASCII em snake_case minúsculo de até 63 caracteres, um separador
_e um sufixo UUIDv7 de 26 caracteres codificado em base32 modificada - O prefixo de tipo evita usar por engano um ID de
useronde é necessário um ID depost, e também permite identificar imediatamente, durante a depuração, a que tipo de entidade o identificador se refere - TypeID é um superconjunto de UUID; ao remover a informação de tipo e decodificar, obtém-se um UUIDv7 válido
- É ordenável em K, podendo ser usado como chave primária de banco de dados, com o objetivo de reduzir o problema de piora de locality no banco em comparação com UUIDv4, que é um ID global totalmente aleatório
- A codificação em base32 oferece segurança para URL, insensibilidade a maiúsculas e minúsculas, evita caracteres ambíguos, permite seleção com duplo clique e tem comprimento menor que a codificação hexadecimal tradicional do UUID
- Implementações oficiais são fornecidas em Go, SQL e TypeScript, e a versão mais recente da especificação é a v0.3.0
- Com a ferramenta de linha de comando, é possível gerar TypeIDs, decodificar o UUID de um TypeID existente e codificar um UUID em TypeID
typeid new prefixpara gerar um novo TypeIDtypeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41para exibir o tipo e o uuidtypeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881para gerar um TypeID
- No TypeID Converter da Jetify, é possível converter uma string TypeID em UUID ou converter para TypeID no formato
prefix:UUID
1 comentários
Comentários no Hacker News
Algumas sugestões: seria bom fixar e documentar a string de prefixo antes que fique tarde demais
Pela implementação em Go, parece ok usar ASCII minúsculo, mas isso fica ambíguo para tipos compostos como
article-commentEm projetos complexos ou com ORM, é difícil evitar separadores, então valeria considerar permitir um único separador
A implementação em Go não tem testes, mas como o código é fácil de testar em nível unitário, isso deveria ser adicionado sem falta
Em Go, o mais correto seria seguir a implementação de UUID do Google, com uma função de parsing adequada e um array interno de bytes; string deveria ser usada só para renderização e prefixo
O parsing atual é permissivo demais e parece entrar em modo de geração quando o suffix está vazio; além disso, fazer indexação depois de
SplitNpode causar panic se não houver sublinhadoTentei encontrar falhas no design, mas no fim parece que acertaram bem o sweet spot dos trade-offs
Ainda assim, a crítica é válida, e conforme surgirem implementações em mais linguagens, vamos adicionar um conjunto de testes mais rigoroso para verificar a compatibilidade entre elas
Sobre o prefixo, fiquei curioso se a preocupação é o fato de o conjunto de caracteres permitidos não estar definido na especificação
E também deixamos o prefixo mais claro na especificação
O prefixo é definido conforme o domínio de uso
Uso uma abordagem assim há alguns anos, mas com duas diferenças
Primeiro, não considero que as pessoas realmente digitem esses valores à mão, então não me preocupo muito com a confusão entre
le1Em vez disso, uso base32 sem as vogais
eioupara reduzir a chance de formar palavras por acaso, especialmente palavrõesSegundo, adiciono dois caracteres base32 com salt como checksum
Assim, quando o valor está estranho por erro ou má-fé, não preciso nem consultar o armazenamento de dados; não sei por que outras implementações não fazem isso
analritae recebi reclamações por issoTalvez valha considerar incluir
atambém na lista de exclusãoSe a premissa é que humanos não digitam isso manualmente, fico em dúvida sobre qual seria o caminho para um valor incorreto entrar por engano
Pode haver erro de copiar e colar ou uma página renderizar tudo em maiúsculas, mas com base32 dá para normalizar maiúsculas e minúsculas
Também fico curioso sobre como um checksum de 2 bytes, não criptograficamente seguro, ajudaria em caso de má-fé
Estamos avaliando se faz sentido incluí-la na especificação do TypeID
Não sei o quanto isso reduziu consultas ao banco de dados, mas é bom ver que não acabamos em erros mais ambíguos durante a decodificação
Separadamente do texto principal, há um link para o “Crockford's alphabet”: https://www.crockford.com/base32.html
Esse esquema de base32 exclui
I, que se confunde com1,L, que também se confunde com1,O, que se confunde com0, eUNa página, diz-se que o motivo para excluir o
Ué “obscenidade acidental”, mas eu sinceramente não entendo o que isso quer dizerPara criar um bom alfabeto base32 com caracteres alfanuméricos que não causem confusão, basta remover
O,IeLAinda assim sobram 33 caracteres, então é preciso tirar mais um, e como tanto faz qual seja, ele só inventou uma justificativa arbitrária para o último caractere removido
Se for um ID visível ao usuário, não é um motivo totalmente absurdo, mas obviamente está longe de ser uma prevenção perfeita
IeOjá foram removidos e você ainda tira oU, dá para eliminar bastante combinações de três ou quatro letras que podem parecer grosseirasClaro, ainda existem combinações possíveis por causa do
A, mas a chance fica bem baixaUnem existia na época de Cícero, então é uma letra vulgar a ponto de beirar a obscenidadeNa empresa, definimos várias novas “bases” para QR codes, e pessoalmente acho que essa é uma área pouco explorada da ciência da computação
Estávamos resolvendo um problema de URL slug, e como o comprimento ficava relativamente grande, achamos que 5 bits por byte ajudariam a reduzir o incômodo de transcrição
No fim, ainda havia margem para remover mais algumas letras, e depois que todo mundo foi embora, nós dois ficamos sentados classificando palavrões pelo grau de ofensa para decidir quais caracteres tirar
Depois me convenceram de que termos pejorativos eram um problema maior, então passamos a focar em remover
nem vez deutggré só meio fofo, masn**rsignificaria ter várias conversas constrangedoras com equipes de RHO conjunto final de caracteres já está meio nebuloso na minha memória, mas em geral lidávamos com
[a-z][0-9]e havia muitos símbolos impróprios para URL ou difíceis de ditarPelo que lembro, removemos
0,le1, e acho que partíamos do princípio de que a transcrição seria toda em maiúsculas ou toda em minúsculas0onão era um problema, e1Ltambém nãoNão gosto muito da codificação Crockford
Na prática, ao dar suporte técnico ou analisar valores codificados dessa forma, achei que isso foi claramente um erro
Esse alfabeto foi projetado para um objetivo que na prática é raro, como ler identificadores por telefone, e introduz ambiguidade
Em situações em que você precisa dar
grepem logs ou fazer referência cruzada usando a própria string codificada, isso é um desastre, e ainda permite hífens, o que vira uma grande fonte de erros de copiar/colar e de quebra de linhaÉ raro digitar identificadores de objeto manualmente, mas é comum copiar e colar entre aplicativos, chats e fóruns, encaminhar por email e procurar em arquivos de log
Nessas condições, ser pronunciável não tem valor, não diferenciar maiúsculas de minúsculas atrapalha, e o que se precisa é consistência e resistência a colagem e quebra de linha
base58 é uma codificação bijetiva mais adequada para essas necessidades e ainda por cima mais curta
Inspirados pela Stripe, há alguns anos usamos como identificadores de objeto UUIDs codificados em base58 com prefixo de tipo, como
user_1BzGURpnHGn6oNru84B3RiAinda assim, é preciso levar em conta que a codificação base32 do Douglas Crockford foi projetada há 20 anos, em uma época em que o ambiente de uso era bem diferente
Mas eu não queria depender de distinção entre maiúsculas e minúsculas, então no fim acabei pendendo para base32
Por exemplo, você pode querer usar o ID como nome de arquivo, e pode estar preso a um ambiente com sistema de arquivos case-insensitive
O TypeID sempre usa o alfabeto do Crockford em minúsculas; ele não usa o conjunto completo de regras da codificação Crockford
No TypeID, hífens não são permitidos, e também não é permitido codificar o mesmo ID de várias formas trocando caracteres ambíguos
Mas às vezes você digita manualmente um identificador que apareceu em uma captura de tela ou compartilhamento de tela, ou em outro dispositivo do qual não é fácil copiar o identificador
A base32 do Crockford também funciona, mas não me agrada tanto assim
Pessoalmente, minha primeira opção seria usar KSUID com prefixo de tipo
Você obtém um ID de 160 bits ordenável por K com codificação base62, e isso funciona bem a menos que você realmente precise de IDs de 128 bits por questão de compatibilidade
Minha codificação base32 favorita é a z-base-32, que me parece mais agradável aos olhos: https://philzimmermann.com/docs/human-oriented-base-32-encod...
O maior problema da base58 é que ela funciona bem para inteiros, mas é menos adequada para dados binários arbitrários, como chaves criptográficas, e strings com distinção entre maiúsculas e minúsculas não ficam bonitas
Elegante
Gostei do prefixo “type-safe”
No nosso ORM, chamávamos de tagged ids uma abordagem parecida que anexava automaticamente tags por entidade aos IDs inteiros dentro do DB: https://joist-orm.io/docs/advanced/tagged-ids
Usávamos
:como separador, mas me arrependo um pouco de não ter usado_pela facilidade de copiar/colar com clique duploEm teoria, também seria bem fácil fazer o Joist converter uma “coluna UUID do DB” em um “TypeID do modelo de domínio”, mas atualmente isso provavelmente não seria possível só com configuração na camada do usuário
Ainda assim, é uma boa ideia
Os IDs dos elementos têm a forma
t3_15bfi0, et3_é o prefixo de tipot3é post,t1é comentário,t5é subreddit, e o restante é a codificação em base36 da chave primária auto-incremental.)Os dois-pontos eram codificados como
%em URLs, aumentando o tamanho do ID, e as URLs ficavam feias e longas, o que era muito irritanteUUIDv7 é popular no HN há anos, e fico me perguntando quando isso vai virar um padrão de verdade e quando bibliotecas, bancos de dados e o restante do ecossistema vão passar a oferecer suporte nativo
A maioria dos softwares não se importa com bits específicos dentro do UUID, então já dá para usar hoje
Se houver software que se importe com bits específicos, basta fazê-lo parecer um UUIDv4, e esses bits também podem ser gerados aleatoriamente
Se for necessário um procedimento de geração, você mesmo pode implementá-lo, e não é difícil
Por exemplo, eu mantenho a biblioteca UUID de Dart, e a versão beta major mais recente inclui v6, v7 e v8 customizado
Também existe uma lista de implementações em algum lugar, e eu apareço naquela página como mantenedor de biblioteca, então recebo avisos para os autores sempre que sai um novo rascunho
Gostei da parte de “dá para copiar/colar com clique duplo”
Detalhes importam
Tenho algumas reclamações sobre UUID
Não entendo por que precisamos passar por todo esse ritual de versionamento de UUID em vez de simplesmente combinar tempo + aleatoriedade
Se localidade não importa, basta usar 128 bits aleatórios
Pela minha experiência, a maioria das pessoas parece achar que UUID precisa ser armazenado na representação hexadecimal legível por humanos, inclusive com hífens
Isso desperdiça espaço demais em banco de dados, rede e memória
Por exemplo, o ULID https://github.com/ulid/spec é mais curto, armazena o tempo e é ordenável lexicograficamente
Na realidade, como em outros problemas de engenharia, basta listar os requisitos e ver o que resolve o problema
Ler sobre as vantagens de vários formatos ajuda, porque permite aproveitar o que os outros aprenderam com dificuldade
Ordenabilidade e localidade baseada em tempo podem não ser coisas que vêm naturalmente à cabeça, mas, se forem necessárias, é muito melhor saber disso antes do que descobrir quatro anos depois do projeto que você descartou esses dados
Alguns formatos de UUID também introduziram pequenos problemas de segurança por conta própria; por exemplo, vale a pena evitar coisas como o vazamento de endereço MAC no UUID v1
Se uma solução existente servir para o caso de uso certo, é só usá-la
Se não servir, também não é algo com que você precise se preocupar demais
Pessoalmente, no lugar onde mais uso UUID, a especificação foi “mande apenas uma string única; se isso te deixar mais confortável, use a biblioteca de UUID que quiser”
Esse sistema parece ter um formato diferente para cada origem de dados, mas tudo bem
A escala é de dezenas de milhares por dia, então não há problema de escala, e também não há necessidade de ordenar por UUID
Em vez de ser um identificador de fato, isso funciona mais como um token único criado pelo emissor para cada mensagem, usado para detectar chegadas duplicadas a jusante em um sistema heterogêneo com várias filas
Também não há necessidade de unicidade global; basta ser único dentro de um shard pequeno e, em princípio, até poderia se repetir depois de algum tempo
Mas, por simplicidade, mantemos o sistema de forma que seja único ao longo de todo o período
Quando gero isso eu mesmo, leio dados de
/dev/urandom, codifico em base64 e escolhi um tamanho que não termine com==Isso também não foi por um problema real, mas por estética
Mesmo assim, ainda não entendo por que os hífens são necessários, e acho que as outras versões não servem para muita coisa
Se você não precisa necessariamente de UUIDv7 ou UUID, https://github.com/segmentio/ksuid oferece um espaço de chaves muito maior
Se precisar de namespace, basta adicionar um prefixo de string, e a probabilidade de colisão do KSUID é muito menor do que a de qualquer versão de UUID
Entre os geradores de ID de uso geral com timestamp ordenável, o ksuid foi o melhor que encontrei, e há bibliotecas para a maioria das linguagens
UUID v1~v7 desperdiçam muito
Se você puder arcar com os bits extras, e a maioria pode, o KSUID é um ótimo formato
[1] https://github.com/sophiabits/resource-id
Mas o grande problema do ksuid é que ele tem 160 bits, então não se encaixa no tipo UUID nativo dos bancos de dados, por exemplo no PostgreSQL, o que gera custo de performance
1: https://github.com/svix/rust-ksuid
2: https://github.com/svix/python-ksuid
K-ordenável é um conceito excelente, e chaves fracamente ordenadas resolvem vários casos de uso
Também gosto da representação em string compacta com tipo
Mas me preocupo se muitos problemas de segurança vão surgir como efeito colateral não intencional do UUID v7
As pessoas não deveriam usar UUID como token, nem como chave primária do banco de dados, mas na prática fazem isso
UUID v4 é, na prática, um número aleatório criptográfico, então acho que até agora muitas vulnerabilidades de segurança acabaram passando ilesas por sorte
No UUID v7, eu tinha entendido que os dados realmente aleatórios caíam para 32 bits, e achava que seria preciso educar bem os desenvolvedores sobre o fato de UUIDs poderem ser adivinháveis
Correção: parece que eu li errado sobre a previsibilidade do UUID v7
O rascunho recomenda CSPRNG para os bits aleatórios, diz que a entropia é de no mínimo 74 bits e que ele foi projetado para ser “imprevisível”
Para usos relacionados à segurança ele manda usar “UUID v4”, mas isso provavelmente pode ter a ver com o significado ligado ao timestamp
Se os nomes são UUIDv4 e UUIDv7, isso não vai gerar uma confusão sem fim, em que é preciso ficar lembrando qual é melhor para banco de dados e qual é melhor para token de uso único?
Também não vejo uma boa solução com compatibilidade retroativa
Em Elixir, usa-se a função
UUID.uuid4()para gerar UUID v4Em teoria daria para varrer o código procurando esses usos, mas tudo isso só aumenta a chance de erro
O mesmo acontecia com os baseados em hash, como o v3
v4 é simples e menos propenso a uso indevido