wandres.dev
EL REGISTRO NPM · publicar paquetes

Paquetes scoped: @org/pkg, acceso y publishConfig

Los paquetes con scope como namespace del registro: la diferencia entre acceso público y restringido, por qué falla la primera publicación con scope, cómo las organizaciones agrupan equipos y permisos, y cómo publishConfig declara el modo de publicación sin depender de flags frágiles en la línea de comandos.

⏱ 16 min

Un scope es más que un prefijo bonito: es un espacio de nombres que resuelve de golpe el problema del name squatting, agrupa paquetes bajo una identidad y decide a qué registro se enruta cada dependencia. Pero trae una trampa que tumba la primera publicación de casi todo el mundo: los paquetes con scope nacen privados. Entender el acceso, las organizaciones y publishConfig es lo que convierte esa fricción en una palanca de control.

🎯 Al terminar esta lección sabrás
  • Comprender qué es un scope como namespace y cómo mapea a node_modules y al enrutado del registro.
  • Distinguir el acceso público del restringido y por qué falla la primera publicación con scope.
  • Declarar el modo de publicación con publishConfig en lugar de flags sueltos en el terminal.
  • Modelar permisos con organizaciones, equipos y roles sobre paquetes concretos.

Qué es un scope

Un scope es un prefijo con arroba que actúa como namespace: @acme/ui es el paquete ui dentro del scope @acme. Hay dos clases, indistinguibles en sintaxis pero distintas en gobernanza: el scope de usuario, que coincide con tu nombre en el registro, y el scope de organización, propiedad de una cuenta colectiva.

En disco, el scope se materializa como carpeta: un paquete con scope vive en node_modules/@acme/ui, no en la raíz plana de node_modules. Esa carpeta con arroba es la huella visible del namespace.

👤

Scope de usuario

Coincide con tu nombre en el registro: @tu-usuario/pkg. Ideal para proyectos personales y experimentos sin montar una organización.

🏢

Scope de organización

Propiedad de una cuenta colectiva: @acme/pkg. Añade equipos, roles y políticas de seguridad compartidas sobre todos sus paquetes.

El valor estructural del scope se despliega en tres frentes que el espacio plano deja abiertos:

  • Colisiones: @acme/parser y @otra/parser conviven sin disputarse el nombre global parser, un recurso escaso.
  • Enrutado: un scope entero puede apuntar a otro registro desde el .npmrc, sin tocar el resto.
  • Gobernanza: es la unidad natural sobre la que colgar permisos, equipos y doble factor.

Ese mapeo por scope es la costura que hace posible mezclar paquetes públicos y privados sin ambigüedad: @acme resuelve contra tu servidor interno mientras el resto sigue yendo al registro público.

# todo el scope @acme se resuelve contra un registro interno
@acme:registry=https://npm.acme.internal/
//npm.acme.internal/:_authToken=${ACME_TOKEN}
📝
El scope más específico gana

El enrutado del .npmrc sigue una regla simple: una línea como @acme:registry tiene prioridad sobre el registry global para cualquier paquete de ese scope, y todo lo demás cae al registro por defecto. Así conviven sin ambigüedad un scope interno apuntando a tu servidor privado, el scope @jsr yendo a JSR y el resto del árbol resolviéndose contra el registro público: cada dependencia sabe exactamente a qué servidor preguntar.

Acceso: público frente a restringido

Aquí está el escollo que sorprende a todos. Los paquetes sin scope son siempre públicos: no existe otra opción. Los paquetes con scope, en cambio, tienen acceso restricted (privado) por defecto.

El acceso se reduce a dos valores, y lo decisivo es que su defecto depende del scope:

  • public: visible e instalable por cualquiera; único valor posible sin scope.
  • restricted: privado; el defecto de los paquetes con scope, y exige plan de pago.

La consecuencia es brutalmente concreta: tu primer npm publish de un @tu-usuario/paquete falla con un 402 Payment Required o un 403, porque el registro asume que quieres publicarlo en privado y eso cuesta dinero.

La solución no es un plan de pago: es declarar explícitamente que lo quieres público. Ese único flag convierte el fallo en una publicación abierta y gratuita.

# publica un paquete con scope como publico y gratuito
npm publish --access public

# publicar como privado requiere una organizacion de pago
npm publish --access restricted

El flag --access public es lo que convierte un scope en un paquete abierto sin coste, y funciona igual para scopes de usuario y de organización. La lección de fondo es que el registro trata “con scope” como señal de intención privada, y te obliga a firmar de forma consciente la decisión de abrirlo: es más grave publicar algo privado por error que fallar al publicar algo público.

⚠️
El 402 no significa que necesites pagar

El fallo más frecuente al estrenar un paquete con scope es leer Payment Required y creer que el registro exige dinero para publicar código abierto. No es así: publicar paquetes con scope de forma pública es gratis. El registro solo asumió restricted por defecto. Pasa --access public una vez —o mejor, decláralo en publishConfig— y el 402 desaparece. Guardarlo en el manifiesto evita que un colaborador tropiece con lo mismo mañana.

publishConfig: la publicación declarativa

Depender de recordar flags en cada publicación es frágil: un despiste y publicas con el acceso equivocado, o contra el registro equivocado. La forma robusta es declarativa.

El objeto publishConfig dentro de package.json fija, junto al código, cómo debe publicarse el paquete. Es la fuente de verdad versionada, revisable e inmune a los olvidos del terminal.

{
  "name": "@acme/ui",
  "version": "1.4.0",
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/",
    "provenance": true
  }
}

Dentro de publishConfig caben las decisiones que no quieres dejar al azar:

  • access: public o restricted, sin depender del flag.
  • registry: el destino, para no publicar por error contra el registro equivocado.
  • tag: el dist-tag por defecto de esta publicación —lo verás en la próxima lección—.
  • provenance: activa la firma de procedencia desde CI.

Con esas claves en el manifiesto, el comando de publicación se queda desnudo: ya no necesita recordar ningún flag, porque cada decisión vive en el package.json.

# con publishConfig en el manifiesto, publicar no necesita flags
npm publish   # toma access, registry, tag y provenance del package.json

Llevar estas decisiones del terminal efímero al manifiesto versionado las vuelve documentadas y reproducibles: quien clone el repositorio publica igual que tú, sin heredar un ritual oral de flags.

Organizaciones: equipos, roles y permisos

Las organizaciones son la capa de gobernanza sobre los scopes. Una organización posee un scope y modela quién puede qué mediante equipos y roles:

  • Propietario: control total, incluida la facturación y el borrado de la organización.
  • Administrador: gestiona miembros, equipos y paquetes, sin tocar la facturación.
  • Miembro: accede a los paquetes según los permisos de sus equipos.

Los permisos se asignan por paquete —lectura o escritura— a través de equipos, y la organización puede exigir doble factor a todo el que publique.

Así, el equipo de diseño escribe en @acme/ui mientras el de datos solo lo lee, y ninguna publicación ocurre sin segundo factor. El scope deja de ser un prefijo para convertirse en una frontera organizativa con política real.

flowchart TD
O[organizacion acme] --> T1[equipo diseno]
O --> T2[equipo datos]
T1 -->|escritura| P1[paquete acme ui]
T2 -->|lectura| P1
T2 -->|escritura| P2[paquete acme metrics]
style O fill:#89b4fa,color:#11111b
style P1 fill:#a6e3a1,color:#11111b
style P2 fill:#a6e3a1,color:#11111b
El scope es una frontera, no un adorno

Quien ve @acme/ como decoración estética se pierde su verdadera naturaleza: es simultáneamente un namespace que impide colisiones, una unidad de enrutado que decide contra qué registro resuelve cada dependencia, y una frontera de gobernanza que agrupa permisos, equipos y políticas de seguridad. Las tres facetas convergen en un mismo prefijo. Por eso el ecosistema moderno empuja hacia scopes por defecto: un scope te da un nombre imposible de disputar, la posibilidad de partir el árbol de dependencias entre lo público y lo interno con una sola línea de .npmrc, y un lugar natural donde colgar permisos y doble factor. El valor por defecto restringido, que tanto frustra al principiante, es la prueba de que el diseño toma en serio la seguridad: prefiere que falles al publicar antes que exponer por accidente. Y publishConfig cierra el círculo llevando esa decisión —público o privado, este registro o aquel— del terminal efímero al manifiesto versionado, donde queda documentada, revisable y a prueba de olvidos. Dominar el scope es dejar de pensar en nombres de paquete para empezar a pensar en espacios de nombres con política: colisiones resueltas, enrutado controlado y permisos explícitos, todo colgando de un mismo prefijo con arroba.

⚔️ Domina el namespace
  1. Crea un paquete @tu-usuario/practica e intenta npm publish sin flags; observa el 402 o 403 y explica su causa.
  2. Publícalo de nuevo con --access public y confirma que el registro lo acepta.
  3. Mueve esa decisión a publishConfig con access y registry, y comprueba que ya no necesitas el flag.
  4. Añade una entrada de scope en un .npmrc que enrute @acme a un registro distinto del público.
  5. Diseña sobre papel una organización con dos equipos y asigna permisos de lectura y escritura por paquete.