2 puntos por GN⁺ 2023-06-29 | 1 comentarios | Compartir por WhatsApp
  • 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_case y 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 user donde se necesita uno de post, 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 prefix genera un nuevo TypeID
    • typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 muestra el type y el uuid
    • typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881 genera 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

 
GN⁺ 2023-06-29
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-comment
    En 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 SplitN podría causar un pánico si no hay guion bajo
    Intenté encontrarle fallas al diseño, pero al final parece que dio muy bien con el sweet spot de los trade-offs

    • Las pruebas sí existen en la parte más compleja, como la codificación base32: https://github.com/jetpack-io/typeid-go/blob/main/base32/bas...
      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
    • Como en el README se piden otras implementaciones, un conjunto de pruebas también vendría bien para código de terceros
    • Después implementé pruebas bastante exhaustivas como seguimiento: https://github.com/jetpack-io/typeid-go/blob/main/typeid_tes...
      Y también dejé claro el prefijo en la especificación
    • Voy a intentar escribir pruebas para esto
    • Parece que se entendió mal como si el prefijo fuera algo fijo
      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 l y 1
    En cambio, uso base32 sin las vocales eiou para reducir la posibilidad de que aparezcan palabras por accidente, especialmente insultos
    Segundo, 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

    • Hace unos años me reclamaron porque una contraseña autogenerada salió como analrita
      Tal vez valga la pena considerar agregar a a los caracteres excluidos
    • Estoy de acuerdo con agregar checksum, pero me genera duda la parte de “por error o con mala intención”
      Si 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
    • La idea del checksum es interesante
      Estoy evaluando si corresponde incluirla en la especificación de TypeID
    • Hace unos meses implementé el segundo enfoque como parte del método de codificación
      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 I y L, que se confunden con 1, O, que se confunde con 0, y también U
    La página dice que la razón para quitar U es la “obscenidad accidental”, pero no entiendo qué se supone que significa

    • Es algo que Crockford escribió en tono juguetón
      Para hacer un buen alfabeto base32 con caracteres alfanuméricos que no se presten a confusión, basta con quitar O, I y L
      Aun 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
    • Si ya excluyes I y O y además quitas U, puedes eliminar bastantes combinaciones de tres o cuatro letras que se ven algo toscas
      Claro, como sigue estando A, aún quedan combinaciones posibles, pero la probabilidad es muy baja
    • Para un verdadero purista del latín, la U no existía en tiempos de Cicerón, así que es una letra tan vulgar que roza lo obsceno
    • También hay más de estas cosas: base58 al estilo de Satoshi/Bitcoin https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58, “base62” de saltpack de Keybase https://github.com/keybase/saltpack, el famoso “Adobe 85” https://en.wikipedia.org/wiki/Ascii85, basE91 https://base91.sourceforge.net
      En 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
    • Unos 4 años antes que Crockford, un colega y yo pensamos en un conjunto de caracteres casi idéntico
      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 n en vez de u
      tggr era hasta tierno, pero n**r implicaba tener conversaciones incómodas con varios equipos de HR
      El 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 dictar
      Si mal no recuerdo, quitamos 0, l y 1, y creo que asumíamos que toda transcripción manual se haría o bien toda en mayúsculas o bien toda en minúsculas
      0o no era problema, y 1L tampoco
  • La 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 grep en 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ínea
    Rara 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_1BzGURpnHGn6oNru84B3Ri
    De 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

    • También consideramos base58 y base64url, y nos gustaban las codificaciones más cortas
      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
    • Coincido en que leer identificadores por teléfono es raro
      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
    • base62 como la que usan base58 o KSUID tiene muchas ventajas
      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
    • No sé de dónde sale lo de los guiones en Crockford Base32: https://en.wikipedia.org/wiki/Base32#Crockford's_Base32
      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 clic
    En 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

    • Reddit también hace algo parecido, pero optimizado para la longitud de la cadena
      Los IDs de elementos tienen forma como t3_15bfi0, donde t3_ es el prefijo de tipo
      t3 es una publicación, t1 es un comentario, t5 es un subreddit, y el resto es la codificación en base36 de la clave primaria autoincremental
    • El sistema interno de IDs tipados de nuestra empresa al principio usaba dos puntos como separador, pero pronto lo cambiamos por punto (.)
      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 molesto
  • UUIDv7 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

    • No sé qué tipo de soporte estás esperando
      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
    • Parece estar en la etapa final de estandarización en la IETF: https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122b...
    • Ha pasado por borradores y mejoras, está muy cerca de estandarizarse, y muchas librerías ya lo soportan o están agregando nuevas funciones
      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

    • Mucha gente ha pensado lo mismo
      Por ejemplo, ULID https://github.com/ulid/spec es más corto, guarda el tiempo y además se ordena lexicográficamente
    • Del lado de UUID suele haber una tendencia a hablar como si un solo caso de uso representara a todos, y por lo tanto existiera un único mejor formato UUID
      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
    • Combinar tiempo y aleatoriedad no puede garantizar unicidad global
    • Personalmente, el único estándar UUID que considero valioso es v4, que es aleatoriedad pura
      Aun así, sigo sin entender por qué hacen falta los guiones, y me parece que las otras versiones aportan poco
    • Lo peor de los guiones es que no te dejan copiar/pegar fácilmente todo con doble clic
  • 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

    • Publiqué un paquete ligero de JavaScript que genera KSUID con prefijo de tipo
      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
    • La razón para cambiarse a ULID fue que siempre está en minúsculas, así que puede tratarse sin distinguir mayúsculas y minúsculas
    • Mantengo y uso en producción varias librerías populares de ksuid, así que me gusta ksuid
      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

    • Hay un problema de usabilidad interesante
      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
    • Sí veo algunos casos donde podría servir, pero en el pasado cada vez que me encontré con UUID parcialmente secuenciales como v1 o v5, estaban siendo mal usados
      Lo mismo pasaba con los basados en hash como v3
      v4 es simple y tiene menos posibilidades de uso incorrecto