- Mientras usaban la API de GitHub, surgió un problema en la función para generar enlaces de comentarios de PR: los enlaces no funcionaban por una discrepancia de ID
- Tras investigar, descubrieron que GitHub usa en paralelo dos sistemas de ID: el node ID de GraphQL y el database ID de la API REST
- Al decodificar en base64 el node ID, confirmaron que los 32 bits inferiores incluyen el database ID, por lo que se puede convertir con una simple operación de bitmask
- Un análisis adicional reveló que GitHub mezcla un nuevo formato de ID basado en MessagePack con un formato legado basado en cadenas
- Esta estructura muestra la dualidad del sistema interno de identificación de objetos de GitHub, y que los desarrolladores deben tener cuidado al integrar APIs
Descubrimiento del sistema dual de ID de GitHub
- Durante el desarrollo de una función del tool de revisión de código con IA de Greptile, apareció un problema en el que los enlaces a comentarios de PR de GitHub no funcionaban
- Guardaban el ID del comentario y lo conectaban a la URL, pero al hacer clic no se abría la página de GitHub
- Al revisar la documentación de GitHub, encontraron que el node ID de la API GraphQL y el database ID de la API REST existen como sistemas distintos
- Ejemplo de node ID:
PRRC_kwDOL4aMSs6Tkzl8
- Ejemplo de database ID:
2475899260
- El node ID es una cadena codificada en base64 para identificar objetos globalmente dentro de GitHub, mientras que el database ID se usa como identificador entero en URL
Análisis de la relación entre node ID y database ID
- Al comparar los node ID y database ID de varios comentarios de PR, confirmaron que ambos valores aumentan con un patrón constante
- Al decodificar la parte base64 del node ID, obtuvieron un entero de 96 bits, y los 32 bits inferiores coincidían con el database ID
- Ejemplo:
PRRC_kwDOL4aMSs6Tkzl8 → 32 bits inferiores = 2475899260
- Es posible extraer el database ID con una operación simple de bitmask
- La conversión se hace con una operación como
decoded & ((1 << 32) - 1)
El formato legado de ID de GitHub
- Al decodificar el node ID de un repositorio antiguo (
torvalds/linux), apareció una cadena con un formato distinto
- Ejemplo:
MDEwOlJlcG9zaXRvcnkyMzI1Mjk4 → 010:Repository2325298
- Este formato sigue la estructura
[número de tipo de objeto]:[nombre del objeto][Database ID], y es un identificador explícito basado en cadenas
- En el caso de objetos árbol, aparece como
04:Tree2325298:7201bfb9..., incluyendo el ID del repositorio y el valor SHA
- GitHub usa en paralelo el formato legado y el nuevo formato, y el formato cambia según el tipo de objeto y el momento en que fue creado
Estructura del nuevo formato de node ID
- La guía de migración de GraphQL de GitHub indica que el node ID debe tratarse como una cadena opaca, pero sí existe una estructura interna
- Tras decodificar en base64 y desempaquetar con MessagePack, aparece un arreglo de datos
- Ejemplo:
[0, 47954445, 2475899260]
- Composición del arreglo
- Primer elemento (0): se estima que es un identificador de versión
- Segundo elemento (47954445): el database ID del repositorio
- Tercer elemento (2475899260): el database ID del objeto
- La longitud del arreglo cambia según el tipo de objeto; los commits incluyen el SHA y los repositorios solo incluyen dos elementos
Uso práctico y conclusión
1 comentarios
Comentarios en Hacker News
El GitHub global node ID más reciente se puede forzar mediante el header
'X-Github-Next-Global-ID'El ID está compuesto por el prefijo de tipo del objeto y un payload msgpack codificado en base64
Por ejemplo, mi ID de usuario
"U_kgDOAAhEkg"se decodifica como[0, 541842], lo que coincide con eldatabaseIdde la API RESTPero es mejor no depender de este tipo de implementación interna y consultar directamente el campo
databaseIden la API GraphQLDocumentación relacionada: guía de migración de GraphQL global node IDs, mi información de usuario de GitHub, ejemplo de decodificación en CyberChef, implementación de GitHub ETag
Decodificarlo de esta forma me parece frágil
Los global node IDs de GraphQL, en principio, deberían ser opacos (opaque)
Varios tipos de GitHub (como PullRequest) exponen el campo
databaseId, así que lo correcto es usar esoLa mayoría de las APIs GraphQL codifican en base64 el nombre del tipo y el ID de la BD, pero no hay garantía de que esa convención se mantenga para siempre
Referencia: documentación del objeto PullRequest, especificación de GraphQL global IDs
permalink,urly la interfazUniformResourceLocatable, así que no hace falta construir URLs manualmentePara eso existe que la API entregue permalink. Los IDs o patrones de links pueden cambiar en cualquier momento
Este enfoque también se usa mucho en tokens de paginación
IDs como
010:Repository2325298tienen una estructura clara010es el enum del tipo,Repositoryes el nombre y2325298es el ID de la BDO sea, es una forma de prefijo de longitud (length prefix). Repository tiene 10 caracteres, Tree tiene 4
Opus 4.5 conoce este truco para decodificar IDs de GitHub y escribe automáticamente el código para hacerlo
Lo que descubrió el autor es técnicamente correcto, pero no está documentado ni soportado
GitHub ya ha cambiado silenciosamente la estructura interna de los node IDs en el pasado
Si agregan campos al arreglo MessagePack, cambian la codificación, la cifran o la cambian por algo basado en UUID,
cualquier sistema que dependa de esa estructura interna se rompe de inmediato
Los identificadores de GitHub que guardo explícitamente son más bien claves URL inmutables (número de issue/PR o hash de commit)
El ID de comentario simplemente lo meto dentro de un blob JSON
No hace falta intentar normalizarlo todo. JSON es suficientemente rápido
A menos que hagas consultas cruzadas por comentario, casi nunca se va a notar como un problema de rendimiento
Si el repositorio cambia de nombre o se mueve a otra organización, la URL puede cambiar
En la vieja API v3 no había IDs, así que si alguien cambiaba su nombre de usuario o de repositorio era difícil seguirle el rastro
Por eso implementé yo mismo un sistema de gestión de ownership por equipos
Como el provider de Terraform no era muy bueno, al hacer offboarding pasaban seguido cosas como “se fue la única persona que era admin”
Todos los repositorios pertenecen a un equipo y los permisos de acceso también se asignan solo a nivel de equipo
Este tipo de control de acceso basado en equipos sirve no solo para GitHub, sino también para otros sistemas
Es un caso típico de Hyrum’s Law — cuando la gente empieza a depender de comportamientos no documentados, tarde o temprano algo se rompe
En diseño de bases de datos, normalmente se entrega hacia afuera una clave natural opaca y por dentro se usa un ID entero incremental
Pero si usas IDs compuestos, estos problemas se reducen.
Por ejemplo, si dentro del ID de un repositorio va incluido el ID del objeto, al incrementar el ID solo se exploran objetos dentro de ese mismo repositorio
Si además le mezclas entropía o timestamps, se vuelve casi imposible de explotar
Por eso es más seguro exponer una surrogate key sin significado
Por ejemplo, YouTube puede usar internamente un número de índice, pero hacia afuera entrega IDs en forma de código sin significado
Ahora se entiende por qué el equipo de GitHub ha ampliado tanto el soporte de Rails para sharded/multi-database en los últimos años