TypeID - identificadores globalmente únicos, type-safe y ordenables en K inspirados en los IDs de Stripe
(github.com/jetpack-io)- TypeID es un formato de identificador type-safe que extiende UUIDv7, inspirado en el uso de prefijos en la API de Stripe para identificadores globalmente únicos
- La cadena canónica está en minúsculas y se compone de tres partes: un prefijo de tipo ASCII en
snake_casey minúsculas de hasta 63 caracteres, un separador_, y un sufijo UUIDv7 de 26 caracteres codificado en base32 modificada - El prefijo de tipo evita usar por error un ID de
userdonde se necesita uno depost, y durante la depuración permite identificar de inmediato a qué tipo de entidad apunta el identificador - TypeID es un superconjunto de UUID: al quitar la información de tipo y decodificarlo, se obtiene un UUIDv7 válido
- Es ordenable en K, por lo que puede usarse como clave primaria de base de datos, con el objetivo de reducir los problemas de locality en la base de datos frente a UUIDv4, que usa IDs globales completamente aleatorios
- La codificación base32 ofrece seguridad para URL, insensibilidad a mayúsculas y minúsculas, evita caracteres ambiguos, permite selección con doble clic y tiene una longitud más corta que la codificación hexadecimal tradicional de UUID
- Las implementaciones oficiales están disponibles en Go, SQL y TypeScript; la versión más reciente de la especificación es la v0.3.0
- Con la herramienta de línea de comandos se puede generar un TypeID, decodificar el UUID de un TypeID existente y codificar un UUID como TypeID
typeid new prefixgenera un nuevo TypeIDtypeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41muestra el type y el uuidtypeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881genera un TypeID
- En TypeID Converter de Jetify se puede convertir una cadena TypeID a UUID o convertir a TypeID con el formato
prefix:UUID
1 comentarios
Comentarios de Hacker News
Hay algunas sugerencias: convendría fijar y documentar la cadena de prefijo antes de que sea tarde
Viendo la implementación en Go, parece bien que sea ASCII en minúsculas, pero queda ambiguo para tipos compuestos como
article-commentEn proyectos complejos o con ORM es difícil evitar separadores, así que valdría la pena considerar permitir un único separador
La implementación en Go no tiene pruebas, pero como es código fácil de probar unitariamente, definitivamente deberían agregarlas
En Go, lo correcto sería usar una función de parseo adecuada y un arreglo interno de bytes, como en la implementación de UUID de Google, y dejar las cadenas solo para el renderizado y el prefijo
El parseo actual es demasiado permisivo y, si el sufijo está vacío, parece entrar en modo de generación; además, el indexado después de
SplitNpodría causar un pánico si no hay guion bajoIntenté encontrarle fallas al diseño, pero al final parece que dio muy bien con el sweet spot de los trade-offs
Aun así, la observación es válida, y a medida que aumenten las implementaciones en varios lenguajes habrá que verificar que sean compatibles entre sí, así que planeo agregar un conjunto de pruebas más estricto
Sobre el prefijo, me pregunto si la preocupación es que no se haya definido en la especificación el conjunto de caracteres permitidos
Y también dejé claro el prefijo en la especificación
El prefijo se define según el dominio de uso
Llevo años usando un enfoque así, pero hay dos diferencias
Primero, no asumo que la gente realmente ingrese estos valores a mano, así que no me preocupa mucho la confusión entre
ly1En cambio, uso base32 sin las vocales
eioupara reducir la posibilidad de que aparezcan palabras por accidente, especialmente insultosSegundo, agrego dos caracteres base32 con sal como checksum
Así no hace falta consultar el almacenamiento de datos cuando el valor está mal por error o con mala intención, y no entiendo por qué otras implementaciones no hacen esto
analritaTal vez valga la pena considerar agregar
aa los caracteres excluidosSi se asume que las personas no los ingresan a mano, no queda claro por qué vía entraría un valor incorrecto por accidente
Puede haber errores de copiar/pegar o páginas que rendericen en mayúsculas, pero si es base32 se puede normalizar mayúsculas y minúsculas
Y también me pregunto cómo ayudaría frente a casos maliciosos un checksum de 2 bytes que no es criptográficamente seguro
Estoy evaluando si corresponde incluirla en la especificación de TypeID
No sé cuánto habrá reducido las consultas a la base de datos, pero me gusta que durante la decodificación no termine en errores más ambiguos
Aparte del texto principal, hay un enlace a “Crockford's alphabet”: https://www.crockford.com/base32.html
Este esquema base32 excluye
IyL, que se confunden con1,O, que se confunde con0, y tambiénULa página dice que la razón para quitar
Ues la “obscenidad accidental”, pero no entiendo qué se supone que significaPara hacer un buen alfabeto base32 con caracteres alfanuméricos que no se presten a confusión, basta con quitar
O,IyLAun así quedan 33 caracteres, así que hay que quitar uno más, y como daba igual cuál fuera, simplemente le puso una razón arbitraria al último carácter eliminado
Si el ID va a ser visible para usuarios, tampoco es una razón tan absurda, aunque desde luego no evita nada por completo
IyOy además quitasU, puedes eliminar bastantes combinaciones de tres o cuatro letras que se ven algo toscasClaro, como sigue estando
A, aún quedan combinaciones posibles, pero la probabilidad es muy bajaUno existía en tiempos de Cicerón, así que es una letra tan vulgar que roza lo obscenoEn la empresa definimos varios “sistemas de base” nuevos para códigos QR y, personalmente, me parece un área poco explotada dentro de la informática
Estábamos resolviendo un problema de URL slugs, y como la longitud era bastante grande, nos pareció que 5 bits por byte podía reducir bastante la incomodidad de transcribirlo a mano
Al final nos sobraba margen para quitar algunas letras más, y después de que todos se fueron de la oficina, nos sentamos a clasificar insultos por nivel de ofensa para decidir qué caracteres eliminar
Más adelante convencí al resto de que las expresiones despectivas eran un problema mayor, así que nos enfocamos en quitar
nen vez deutggrera hasta tierno, peron**rimplicaba tener conversaciones incómodas con varios equipos de HREl conjunto final ya lo recuerdo medio borroso, pero normalmente manejábamos
[a-z][0-9]y había muchos símbolos que no se podían usar en URL o eran difíciles de dictarSi mal no recuerdo, quitamos
0,ly1, y creo que asumíamos que toda transcripción manual se haría o bien toda en mayúsculas o bien toda en minúsculas0ono era problema, y1LtampocoLa codificación de Crockford no me gusta mucho
De hecho, cuando he tenido que dar soporte técnico o analizar valores codificados de esta manera, me ha parecido claramente un error
Este alfabeto fue diseñado para un objetivo que en la práctica es raro, como leer identificadores por teléfono, y a cambio introduce ambigüedad
Cuando necesitas hacer
grepen logs o cruzar referencias usando la propia cadena codificada, es un desastre, y además permitir guiones se vuelve una fuente importante de errores de copiar/pegar y de saltos de líneaRara vez alguien teclea identificadores de objetos directamente, pero sí es común copiarlos y pegarlos entre aplicaciones, chats y foros, enviarlos por correo y buscarlos en archivos de log
En esas condiciones, que sea pronunciable no aporta nada, que no distinga mayúsculas y minúsculas estorba, y lo que hace falta es consistencia y resistencia al pegado y a los saltos de línea
base58 es una codificación biyectiva que encaja mejor con esos requisitos y además es más corta
Inspirados por Stripe, desde hace años usamos como identificadores de objetos UUID codificados en base58 con prefijo de tipo, como
user_1BzGURpnHGn6oNru84B3RiDe todos modos, hay que tener en cuenta que la codificación base32 de Douglas Crockford fue diseñada hace 20 años, en una época en la que el entorno de uso era bastante distinto
Pero no queríamos depender de distinguir mayúsculas y minúsculas, así que al final nos inclinamos por base32
Por ejemplo, puede que quieras usar el ID como nombre de archivo, y podrías estar atado a un entorno con un sistema de archivos que no distingue mayúsculas de minúsculas
TypeID siempre usa el alfabeto de Crockford en minúsculas, no todas las reglas de la codificación Crockford
TypeID no permite guiones, ni tampoco permite codificar el mismo ID de varias maneras usando caracteres ambiguos alternativos
Pero a veces sí hay que introducirlos manualmente, por ejemplo desde una captura de pantalla o una pantalla compartida, o desde otro dispositivo donde no es fácil copiar el identificador
Crockford base32 funciona, pero no me encanta
Personalmente, mi primera opción sería usar KSUID con prefijo de tipo
Obtienes un ID de 160 bits ordenable por K con codificación base62, y encaja bien salvo que por compatibilidad necesites sí o sí un ID de 128 bits
Mi codificación base32 favorita es z-base-32, me parece más cómoda a la vista: https://philzimmermann.com/docs/human-oriented-base-32-encod...
El mayor problema de base58 es que va bien para enteros, pero encaja peor con datos binarios arbitrarios como claves criptográficas, y que las cadenas sensibles a mayúsculas y minúsculas no se ven muy bien
Está prolijo
Me gusta el prefijo de “seguridad de tipos”
En nuestro ORM hacíamos algo parecido, agregando automáticamente etiquetas por entidad a los IDs enteros dentro de la DB; a eso le llamábamos tagged ids: https://joist-orm.io/docs/advanced/tagged-ids
Usábamos
:como separador, pero me arrepiento un poco de no haber usado_por el tema de copiar/pegar con doble clicEn teoría, sería muy fácil hacer que Joist convierta una “columna UUID de la DB” en un “TypeID del modelo de dominio”, aunque ahora mismo no parece ser algo configurable solo desde el lado del usuario
Igual, es una buena idea
Los IDs de elementos tienen forma como
t3_15bfi0, dondet3_es el prefijo de tipot3es una publicación,t1es un comentario,t5es un subreddit, y el resto es la codificación en base36 de la clave primaria autoincremental.)Los dos puntos se codificaban como
%en el URL encoding, lo que hacía crecer el ID y volvía las URLs feas y largas, algo muy molestoUUIDv7 lleva años siendo popular en HN, y me pregunto cuándo será un estándar de verdad y cuándo lo van a soportar nativamente las librerías, las bases de datos y el resto del ecosistema
La mayoría del software no presta atención a bits específicos dentro de un UUID, así que ya se puede usar hoy
Si hay software que sí depende de ciertos bits, se puede hacer que parezca UUIDv4, y esos bits también se pueden generar de forma aleatoria
Si necesitas el procedimiento de generación, lo escribes tú mismo; no es difícil
Por ejemplo, yo mantengo la librería UUID de Dart, y la última beta del major release incluye v6, v7 y v8 personalizado
También hay una lista de implementaciones en algún lado, y en esa página figuro como mantenedor de librería, así que cada vez que sale un nuevo borrador me avisan a los autores
Me gusta la parte de “se puede copiar/pegar con doble clic”
Los detalles importan
Tengo algunas quejas con UUID
No entiendo por qué hay que pasar por todo el ritual del versionado de UUID en vez de simplemente combinar tiempo + aleatoriedad
Si la localidad no importa, puedes usar directamente 128 bits aleatorios
Por experiencia, parece que la mayoría cree que UUID debe guardarse en su representación hexadecimal legible para humanos, incluso con guiones
Se desperdicia demasiado espacio en bases de datos, red y memoria
Por ejemplo, ULID https://github.com/ulid/spec es más corto, guarda el tiempo y además se ordena lexicográficamente
En la práctica es como cualquier otro problema de ingeniería: anotas los requisitos y ves qué los resuelve
Leer sobre las ventajas de varios formatos ayuda, porque te deja aprovechar lo que otros aprendieron por las malas
La capacidad de ordenar y la localidad basada en tiempo quizá no se te ocurran de forma natural, pero si las necesitas, es mucho mejor saberlo de antemano que darte cuenta 4 años después de que tiraste esos datos
Algunos formatos UUID además introdujeron pequeños problemas de seguridad por sí solos; cosas como la filtración de la dirección MAC en UUID v1 conviene evitarlas
Si la solución existente encaja con tu caso de uso, simplemente úsala
Si no, tampoco hay que preocuparse demasiado
Personalmente, donde más uso UUID, definimos: “manda cualquier cadena única; si te deja más tranquilo, usa la librería UUID que quieras”
En este sistema parece que cada origen de datos tiene su propio formato único, y está bien
La escala es de decenas de miles por día, así que no hay problema de volumen, y tampoco hace falta ordenar por UUID
No es tanto un identificador real como un token único que el emisor crea para cada mensaje, y sirve para detectar entregas duplicadas aguas abajo en un sistema heterogéneo con varias colas
Ni siquiera hace falta unicidad global; basta con que sea único dentro de un shard pequeño y, en principio, no importaría que se repitiera con el tiempo
Aun así, por simplicidad, mantenemos el sistema para que sea único durante todo el período
Cuando lo genero yo mismo, leo datos de
/dev/urandom, los codifico en base64 y elegí un tamaño que no terminara en==Esto tampoco fue por un problema real, sino por estética
Aun así, sigo sin entender por qué hacen falta los guiones, y me parece que las otras versiones aportan poco
Si no necesitas específicamente UUIDv7 o UUID, https://github.com/segmentio/ksuid ofrece un espacio de claves mucho más grande
Si necesitas un namespace, puedes agregar un prefijo de cadena, y la probabilidad de colisión de KSUID es muchísimo menor que la de cualquier versión de UUID
Entre los generadores de ID de propósito general con timestamp ordenable, ksuid fue el mejor, y hay librerías en la mayoría de los lenguajes
UUID v1~v7 desperdician mucho
Si puedes permitirte los bits extra, que en la mayoría de los casos sí, KSUID es un formato excelente
[1] https://github.com/sophiabits/resource-id
Pero el gran problema de ksuid es que tiene 160 bits, así que no encaja en el tipo UUID nativo de bases de datos, por ejemplo en PostgreSQL, y eso genera un costo de rendimiento
1: https://github.com/svix/rust-ksuid
2: https://github.com/svix/python-ksuid
Ordenable en K es un concepto excelente, y las claves débilmente ordenadas resuelven muchos casos de uso
También me gusta la representación compacta en cadena con tipo
Pero me preocupa que como efecto secundario no intencional de UUID v7 aparezcan muchos problemas de seguridad
La gente no debería usar UUID como token ni tampoco como clave primaria de la DB, pero en la práctica sí lo hace
UUID v4 es en esencia aleatorio criptográfico, así que creo que muchas vulnerabilidades de seguridad hasta ahora se salvaron por pura suerte
En UUID v7, yo entendía que los datos realmente aleatorios se reducían a 32 bits, y pensaba que había que educar bien a los desarrolladores sobre que los UUID podían ser adivinables
Corrección: parece que interpreté mal la posibilidad de adivinar los UUID v7
El borrador recomienda usar CSPRNG para los bits aleatorios, dice que la entropía es de al menos 74 bits y que está diseñado para ser “no adivinable”
Aunque para usos relacionados con seguridad sí dice que se use “UUID v4”, probablemente por el significado asociado al timestamp
Si los nombres son UUIDv4 y UUIDv7, da la impresión de que habrá una confusión interminable sobre cuál conviene para base de datos y cuál para tokens de un solo uso
Tampoco veo una buena solución que además sea retrocompatible
En Elixir, para crear un UUID v4 se usa la función
UUID.uuid4()En teoría se podría escanear el uso en el código, pero todo eso aumenta la posibilidad de errores
Lo mismo pasaba con los basados en hash como v3
v4 es simple y tiene menos posibilidades de uso incorrecto