@bacano/sdk 1.1.12 → 1.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
- # Gestión de tipos de Content para agentes
1
+ # Gestión segura de Content para agentes
2
2
 
3
3
  `@bacano/sdk/content-management` permite que una herramienta administrativa o
4
- un agente autorizado consulte, cree y mantenga **tipos y componentes** de
5
- Bacano Content. No permite administrar entradas, productos, ventas, clientes ni
6
- ningún otro dato de la empresa.
4
+ un agente autorizado consulte y mantenga **tipos, componentes, grupos, listas
5
+ de opciones, entradas e imágenes** de Bacano Content. Cada capacidad requiere
6
+ un permiso explícito. El acceso no autoriza productos, ventas, clientes,
7
+ configuración general, GraphQL, SQL ni acceso directo a Storage.
7
8
 
8
9
  Este contrato es independiente del cliente público del storefront. Debe usarse
9
10
  solo desde un proceso local o servidor confiable; nunca desde el navegador ni
@@ -13,17 +14,34 @@ dentro del bundle del sitio web.
13
14
 
14
15
  1. En Bacano, abre **Sitios web → Contenido → Tipos** o **Componentes**.
15
16
  2. Selecciona **Conectar agente**.
16
- 3. Elige 15, 30 o 60 minutos y genera el acceso.
17
+ 3. Elige el alcance, 15, 30 o 60 minutos y genera el acceso.
17
18
  4. Copia el token al entorno local del agente.
18
19
 
19
- El token está firmado, pertenece a una sola empresa y solo contiene el alcance
20
- `content_types:manage`. Cada petición vuelve a comprobar que el usuario que lo
21
- generó continúa vinculado a la empresa y conserva `websites.manage`. Al vencer
22
- o perder ese permiso deja de funcionar.
20
+ El token está firmado con un secreto exclusivo, pertenece a una sola empresa y
21
+ usuario y está respaldado por una concesión revocable. Cada petición comprueba
22
+ la firma, vigencia, concesión, scopes exactos y que el usuario continúa
23
+ vinculado a la empresa con `websites.manage`. Al vencer, revocarse o perder ese
24
+ permiso deja de funcionar.
25
+
26
+ Scopes disponibles:
27
+
28
+ - `content_types:manage`: tipos, componentes, listas de opciones, referencias y
29
+ sus ciclos de vida.
30
+ - `content_entries:manage`: grupos, entradas en borrador y sus ciclos de vida.
31
+ - `content_entries:publish`: publicar y modificar contenido publicado; requiere
32
+ también `content_entries:manage`.
33
+ - `content_media:upload`: consultar imágenes de la empresa y cargar JPEG, PNG o
34
+ WebP. Requiere también `content_entries:manage`; no permite borrar archivos.
23
35
 
24
36
  No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
25
37
  que se empaqueten con el storefront.
26
38
 
39
+ Para desplegar Bacano, la API requiere
40
+ `CONTENT_MANAGEMENT_TOKEN_SECRET`: un secreto aleatorio de al menos 32
41
+ caracteres, exclusivo de esta capacidad y distinto de `API_SECRET`, JWT y los
42
+ secretos de Hasura. Debe configurarse como secreto del entorno; nunca se entrega
43
+ al agente. Rotarlo invalida inmediatamente todos los accesos temporales activos.
44
+
27
45
  ## Crear el cliente
28
46
 
29
47
  ```ts
@@ -35,7 +53,7 @@ const content = createContentManagementClient({
35
53
  });
36
54
 
37
55
  const context = await content.getContext();
38
- console.log(context.companyId, context.expiresAt);
56
+ console.log(context.companyId, context.scopes, context.expiresAt);
39
57
  ```
40
58
 
41
59
  El cliente ofrece:
@@ -47,6 +65,77 @@ El cliente ofrece:
47
65
  - `createType(input)` y `updateType(idOrKey, input)`.
48
66
  - `inspectLifecycle(idOrKey, operation)`.
49
67
  - `applyLifecycle(idOrKey, operation)`.
68
+ - `listEntries(filters)`, `getEntry(idOrKey, locale?)`.
69
+ - `createEntry(input)` y `updateEntry(idOrKey, input)`.
70
+ - `inspectEntryLifecycle(idOrKey, operation)`.
71
+ - `applyEntryLifecycle(idOrKey, operation)`.
72
+ - `listGroups()`, `getGroup(idOrKey)`, `createGroup(input)` y
73
+ `updateGroup(idOrKey, input)`.
74
+ - `inspectGroupLifecycle(idOrKey, operation)` y
75
+ `applyGroupLifecycle(idOrKey, operation)`.
76
+ - `listOptionLists()`, `getOptionList(idOrKey)`, `createOptionList(input)` y
77
+ `updateOptionList(idOrKey, input)`.
78
+ - `inspectOptionListLifecycle(idOrKey, operation)` y
79
+ `applyOptionListLifecycle(idOrKey, operation)`.
80
+ - `listMedia(filters)` y `uploadImage(input)` para reutilizar o cargar imágenes
81
+ cuando el acceso incluya `content_media:upload`.
82
+
83
+ ## Idioma, nombres y keys
84
+
85
+ Antes de proponer una estructura, resolver su idioma en este orden:
86
+
87
+ 1. El idioma solicitado expresamente por el usuario.
88
+ 2. La convención predominante del proyecto existente.
89
+ 3. El idioma principal del website.
90
+ 4. Si todavía existe una ambigüedad material, preguntar al usuario.
91
+
92
+ Para un website en español, los nombres visibles y las keys nuevas se crean en
93
+ español salvo instrucción contraria. Esta regla aplica a identificadores
94
+ definidos por la empresa; los valores reservados del SDK como `entry`,
95
+ `component`, `draft`, `website_content`, `text` o `repeater` no se traducen ni
96
+ se reemplazan por valores inventados.
97
+
98
+ Usar estas convenciones:
99
+
100
+ | Elemento | Convención | Ejemplo |
101
+ | --- | --- | --- |
102
+ | Nombre visible | Lenguaje natural, con tildes | `Preguntas frecuentes` |
103
+ | Key de tipo, componente, sección o campo | `snake_case`, sin tildes | `preguntas_frecuentes` |
104
+ | Key o slug público de una entrada | `kebab-case`, sin tildes | `preguntas-frecuentes-inicio` |
105
+
106
+ Las keys deben describir el significado del contenido, no detalles accidentales
107
+ de su implementación visual. Preferir `titulo`, `descripcion`,
108
+ `imagen_principal`, `enlace_contacto`, `preguntas`, `planes` o `pasos`. Evitar
109
+ identificadores opacos como `data`, `content`, `items`, `field1`,
110
+ `step1_title` o `component_2` cuando existe un nombre semántico claro.
111
+
112
+ Para elementos repetidos, modelar un repetible y, cuando corresponda, un
113
+ componente semántico. Por ejemplo, usar `pasos` con un componente `paso` que
114
+ contenga `titulo` y `descripcion`, en vez de crear `paso1_titulo`,
115
+ `paso1_descripcion`, `paso2_titulo` y campos sucesivos.
116
+
117
+ Antes de crear algo nuevo:
118
+
119
+ 1. Consultar `listTypes()` y `getReferences()`.
120
+ 2. Reutilizar una estructura solo cuando conserva el mismo significado y la
121
+ misma forma, no únicamente porque algunos campos coinciden.
122
+ 3. Dar un nombre neutral a una estructura reutilizable, por ejemplo
123
+ `seccion_preguntas_frecuentes`.
124
+ 4. Incluir el contexto en el nombre cuando la estructura sea realmente
125
+ exclusiva, por ejemplo `calculadora_eficiencia_inicio`.
126
+ 5. Evitar nombres demasiado genéricos como `contenido` y nombres que quedarían
127
+ engañosos al reutilizarse, como `contenedor_principal_servicios` para una
128
+ página de blog.
129
+
130
+ Una key activa es un contrato con el storefront. Nunca traducir, regenerar ni
131
+ renombrar automáticamente keys existentes. La convención de idioma aplica a
132
+ estructuras nuevas; cambiar una key utilizada requiere tratarlo como una
133
+ migración explícita y presentar su impacto. Tampoco crear sufijos como `_2` o
134
+ `-2` sin investigar primero el identificador existente.
135
+
136
+ Antes de ejecutar `createType()`, presentar al usuario el nombre, key, idioma,
137
+ uso, campos, componentes y decisión de creación o reutilización. Corregir
138
+ mezclas de idiomas y errores ortográficos antes de materializar el blueprint.
50
139
 
51
140
  ## Flujo seguro para un agente
52
141
 
@@ -71,13 +160,13 @@ como una eliminación estructural y puede requerir
71
160
 
72
161
  ```ts
73
162
  await content.createType({
74
- key: "cta_button",
163
+ key: "boton_llamada_accion",
75
164
  name: "Botón de llamada a la acción",
76
165
  kind: "component",
77
166
  status: "draft",
78
167
  fields: [
79
168
  {
80
- key: "label",
169
+ key: "texto",
81
170
  label: "Texto",
82
171
  type: "text",
83
172
  required: true,
@@ -85,7 +174,7 @@ await content.createType({
85
174
  settings: { isVisible: true, maxLength: 80 },
86
175
  },
87
176
  {
88
- key: "url",
177
+ key: "enlace",
89
178
  label: "Enlace",
90
179
  type: "link",
91
180
  required: true,
@@ -96,38 +185,38 @@ await content.createType({
96
185
  });
97
186
 
98
187
  await content.createType({
99
- key: "home_hero",
188
+ key: "hero_inicio",
100
189
  name: "Hero del inicio",
101
190
  kind: "entry",
102
191
  status: "draft",
103
192
  usages: ["website_content"],
104
- sections: [{ key: "content", title: "Contenido", sortOrder: 1 }],
193
+ sections: [{ key: "contenido", title: "Contenido", sortOrder: 1 }],
105
194
  fields: [
106
195
  {
107
- key: "title",
196
+ key: "titulo",
108
197
  label: "Título",
109
198
  type: "text",
110
199
  required: true,
111
200
  sortOrder: 1,
112
- sectionKey: "content",
201
+ sectionKey: "contenido",
113
202
  settings: { isVisible: true, maxLength: 120 },
114
203
  },
115
204
  {
116
- key: "actions",
205
+ key: "acciones",
117
206
  label: "Acciones",
118
207
  type: "repeater",
119
208
  required: false,
120
209
  sortOrder: 2,
121
- sectionKey: "content",
210
+ sectionKey: "contenido",
122
211
  settings: { isVisible: true, minRows: 0, maxRows: 3 },
123
212
  children: [
124
213
  {
125
- key: "button",
214
+ key: "boton",
126
215
  label: "Botón",
127
216
  type: "component",
128
217
  required: true,
129
218
  sortOrder: 1,
130
- componentKey: "cta_button",
219
+ componentKey: "boton_llamada_accion",
131
220
  settings: { isVisible: true },
132
221
  },
133
222
  ],
@@ -166,10 +255,10 @@ otros usos editoriales son `website_content` y `product_additional`.
166
255
  ## Ciclo de vida
167
256
 
168
257
  ```ts
169
- const preview = await content.inspectLifecycle("home_hero", "delete");
258
+ const preview = await content.inspectLifecycle("hero_inicio", "delete");
170
259
 
171
260
  if (preview.allowed && userConfirmed) {
172
- await content.applyLifecycle("home_hero", "delete");
261
+ await content.applyLifecycle("hero_inicio", "delete");
173
262
  }
174
263
  ```
175
264
 
@@ -178,15 +267,181 @@ eliminar cuando existe contenido histórico. El backend aplica las mismas reglas
178
267
  de dependencias, keys, componentes, secciones y cambios destructivos que el
179
268
  backoffice; el SDK no crea un camino alterno.
180
269
 
270
+ ## Entradas
271
+
272
+ Antes de crear una entrada, confirma que su tipo existe y que la estructura
273
+ aprobada ya está activa. Las entradas nacen en `draft` salvo que el usuario haya
274
+ concedido explícitamente `content_entries:publish`.
275
+
276
+ ```ts
277
+ const existing = await content.listEntries({
278
+ contentType: "hero_inicio",
279
+ locale: "es-CO",
280
+ limit: 100,
281
+ });
282
+
283
+ const current = existing.entries.find(
284
+ (entry) => entry.key === "hero-principal-inicio",
285
+ );
286
+
287
+ if (!current) {
288
+ await content.createEntry({
289
+ contentType: "hero_inicio",
290
+ key: "hero-principal-inicio",
291
+ name: "Hero principal del inicio",
292
+ locale: "es-CO",
293
+ status: "draft",
294
+ content: {
295
+ titulo: "Encuentra lo mejor para cada etapa",
296
+ descripcion: "Compra productos seleccionados para tu familia.",
297
+ },
298
+ });
299
+ } else {
300
+ await content.updateEntry(current.id, {
301
+ contentType: current.contentType.id,
302
+ key: current.key,
303
+ name: current.name,
304
+ locale: current.locale,
305
+ status: "draft",
306
+ content: {
307
+ ...current.content,
308
+ titulo: "Encuentra lo mejor para cada etapa",
309
+ },
310
+ expectedUpdatedAt: current.updatedAt,
311
+ });
312
+ }
313
+ ```
314
+
315
+ Reglas obligatorias:
316
+
317
+ 1. Inventariar por tipo, key y locale antes de crear; nunca fusionar por nombre.
318
+ 2. Usar `expectedUpdatedAt` al actualizar. Un conflicto exige volver a consultar
319
+ y presentar el cambio; no se sobrescribe silenciosamente.
320
+ 3. Enviar el documento completo que debe permanecer. `updateEntry()` reemplaza
321
+ el contenido de la entrada después de aplicar las validaciones oficiales.
322
+ 4. Reutilizar únicamente `fileId` pertenecientes a la misma empresa. Con
323
+ `content_media:upload`, resolverlos mediante `listMedia()` o
324
+ `uploadImage()`; nunca relacionar URLs arbitrarias.
325
+ 5. Rich text usa el JSON controlado por Bacano, nunca HTML crudo.
326
+ 6. Publicar, retirar o modificar una entrada publicada requiere
327
+ `content_entries:publish` y aprobación humana explícita.
328
+ 7. Inspeccionar el ciclo de vida antes de archivar, restaurar o eliminar.
329
+
330
+ La API reutiliza el mismo servicio de guardado del backoffice: campos
331
+ obligatorios, componentes anidados, repetibles, listas, referencias, rich text,
332
+ media y pertenencia a la empresa se validan de forma autoritativa.
333
+
334
+ ## Imágenes
335
+
336
+ Antes de cargar, busca por nombre o texto alternativo para reutilizar la
337
+ biblioteca existente:
338
+
339
+ ```ts
340
+ import { readFile } from "node:fs/promises";
341
+
342
+ const existing = await content.listMedia({ search: "hero juguetes" });
343
+
344
+ const hero = existing.items[0] ?? await content.uploadImage({
345
+ file: await readFile("./hero-juguetes.png"),
346
+ fileName: "Hero juguetes para bebé",
347
+ altText: "Niña jugando con juguetes educativos",
348
+ });
349
+
350
+ console.log(hero.fileId, hero.width, hero.height);
351
+ ```
352
+
353
+ `uploadImage()` envía los bytes al endpoint administrativo de Bacano. El
354
+ servidor comprueba el contenido real, corrige orientación, elimina metadatos no
355
+ necesarios, normaliza a WebP y registra nombre, MIME, tamaño y dimensiones. El
356
+ límite de entrada es 10 MB y 100 megapíxeles. No se admiten animaciones,
357
+ archivos corruptos, SVG, video ni documentos en esta primera versión.
358
+
359
+ El nombre entregado debe describir el asset; Bacano lo normaliza a un filename
360
+ SEO seguro y agrega un sufijo corto para evitar colisiones. Un SHA-256 de los
361
+ bytes WebP normalizados permite reutilizar duplicados dentro de la misma
362
+ empresa. Nunca se deduplica entre empresas.
363
+
364
+ El `altText` de la biblioteca es solo un fallback. Al asociar una imagen a una
365
+ entrada, conserva el texto alternativo contextual que corresponda a ese uso;
366
+ una misma imagen puede significar cosas distintas en dos secciones. Para una
367
+ imagen decorativa el contenido final debe usar `alt=""`.
368
+
369
+ El agente no puede reemplazar ni borrar físicamente media. Una edición genera
370
+ otro asset y cambia explícitamente el `fileId` de la entrada. Esto evita romper
371
+ productos, servicios, branding u otras entradas que todavía usen el archivo.
372
+
373
+ ## Grupos y listas de opciones
374
+
375
+ Los grupos organizan entradas y usan el scope `content_entries:manage`. Las
376
+ listas proporcionan valores controlados para campos `select`, usan
377
+ `content_types:manage` y conservan sus relaciones padre-hijo.
378
+
379
+ ```ts
380
+ const group = await content.createGroup({
381
+ key: "pagina-principal",
382
+ name: "Página principal",
383
+ status: "active",
384
+ });
385
+
386
+ const materials = await content.createOptionList({
387
+ key: "materiales",
388
+ name: "Materiales",
389
+ status: "draft",
390
+ isExtensible: false,
391
+ items: [
392
+ { value: "algodon", label: "Algodón", sortOrder: 1 },
393
+ { value: "lino", label: "Lino", sortOrder: 2 },
394
+ ],
395
+ });
396
+
397
+ await content.updateOptionList(materials.id, {
398
+ key: materials.key,
399
+ name: materials.name,
400
+ status: "active",
401
+ isExtensible: materials.isExtensible,
402
+ items: materials.items.map((item) => ({
403
+ id: item.id,
404
+ value: item.value,
405
+ label: item.label,
406
+ parentItemId: item.parentItemId,
407
+ sortOrder: item.sortOrder,
408
+ isActive: item.isActive,
409
+ metadata: item.metadata,
410
+ })),
411
+ });
412
+ ```
413
+
414
+ `updateGroup()` y `updateOptionList()` reciben el documento completo que debe
415
+ permanecer. Las keys existentes son inmutables. Para retirar elementos,
416
+ archivar, restaurar o eliminar, primero usa la operación `inspect*Lifecycle()`.
417
+ El backend bloquea desactivar o retirar opciones que todavía usan listas
418
+ dependientes y exige `confirmDestructiveChanges` cuando corresponde; nunca
419
+ omitas opciones existentes por accidente en una actualización.
420
+
421
+ ## Seguridad y auditoría
422
+
423
+ - El `companyId` y el usuario provienen del token; no se aceptan desde el body.
424
+ - Cada consulta y mutación está filtrada por la empresa firmada.
425
+ - UUIDs o keys de otra empresa responden como inexistentes y no se modifican.
426
+ - Las concesiones pueden revocarse individualmente desde Bacano.
427
+ - Emisión, revocación y mutaciones quedan en una auditoría inmutable sin guardar
428
+ el bearer token ni el documento completo.
429
+ - Las respuestas del endpoint usan `no-store`, límites de tamaño y rate limit.
430
+ - Las cargas tienen un límite adicional por concesión; no aceptan importación
431
+ por URL ni exponen credenciales o endpoints internos de Storage.
432
+ - Los tokens v2 no son compatibles con tokens anteriores y tienen como máximo
433
+ 60 minutos de vigencia.
434
+
181
435
  ## Límites de esta versión
182
436
 
183
- - No crea ni modifica entradas de Content.
184
- - No carga archivos ni media.
185
- - No crea listas de opciones.
437
+ - La carga administrativa solo admite imágenes raster JPEG, PNG y WebP. SVG,
438
+ animaciones, video y documentos continúan en el backoffice.
439
+ - No elimina ni reemplaza físicamente archivos de la biblioteca.
186
440
  - No funciona desde el cliente público del storefront.
187
441
  - No acepta GraphQL directo ni credenciales administrativas permanentes.
442
+ - No previsualiza borradores desde el storefront público.
188
443
 
189
444
  El resultado esperado del flujo Figma es: el agente propone el blueprint, el
190
- usuario habilita el acceso temporal, el agente crea o ajusta los tipos y
191
- componentes aprobados, y el equipo editorial completa las entradas desde
192
- Bacano.
445
+ usuario habilita únicamente los scopes necesarios, el agente crea o ajusta la
446
+ estructura y prepara entradas en borrador, y una persona revisa antes de
447
+ publicar.
package/README.md CHANGED
@@ -21,20 +21,24 @@ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
21
21
  as a peer dependency. The core client has no runtime dependencies and works
22
22
  without React.
23
23
 
24
- ### Gestión de tipos de Content para agentes
24
+ ### Gestión de Bacano Content para agentes
25
25
 
26
- Un agente autorizado puede consultar, crear y mantener tipos y componentes con
27
- el entrypoint administrativo `@bacano/sdk/content-management`. El acceso se
28
- genera temporalmente desde Bacano, está limitado a una empresa y nunca debe
29
- incluirse en el storefront:
26
+ Un agente autorizado puede consultar y mantener tipos, componentes, grupos,
27
+ listas de opciones y entradas con el entrypoint administrativo
28
+ `@bacano/sdk/content-management`. Cada capacidad exige un scope explícito. El
29
+ acceso se genera temporalmente desde Bacano, está limitado a una empresa y
30
+ nunca debe incluirse en el storefront:
30
31
 
31
32
  ```ts
32
33
  import { createContentManagementClient } from "@bacano/sdk/content-management";
33
34
  ```
34
35
 
35
36
  Lee [`CONTENT_TYPE_MANAGEMENT.md`](./CONTENT_TYPE_MANAGEMENT.md) antes de usarlo.
36
- La guía contiene el flujo de autorización, contratos de campos, referencias,
37
- cambios destructivos y límites de seguridad.
37
+ La guía contiene el flujo de autorización, contratos de campos, entradas y
38
+ media, estados, referencias, cambios destructivos y límites de seguridad. Con
39
+ el scope `content_media:upload`, un agente puede buscar imágenes existentes y
40
+ cargar JPEG, PNG o WebP mediante el contrato oficial; Bacano valida los bytes,
41
+ normaliza a WebP y registra el resultado en la biblioteca de la empresa.
38
42
 
39
43
  ### Auditoría del storefront
40
44
 
@@ -62,8 +66,8 @@ el usuario o agente invoca la skill.
62
66
  ### Implementación desde Figma
63
67
 
64
68
  El SDK también incluye `bacano-figma-web`, la skill oficial para convertir un
65
- diseño Figma en una web informativa o tienda que conserve el cascarón, Static
66
- First y contratos públicos de Bacano:
69
+ diseño Figma en una web informativa, tienda o sitio híbrido que conserve el
70
+ cascarón, Static First y contratos públicos de Bacano:
67
71
 
68
72
  ```bash
69
73
  pnpm exec bacano-figma-web install --agent all
@@ -82,10 +86,11 @@ la presencia del token REST. El OAuth del MCP remoto debe confirmarse dentro del
82
86
  agente con las herramientas de Figma. La extracción se conserva como snapshot
83
87
  normalizado. Cuando el equipo de Content y desarrollo trabajan en tiempos
84
88
  distintos, la skill genera un blueprint de entrega y permite fixtures locales
85
- con el mismo contrato de los componentes. Las entradas reales continúan
86
- creándose desde Bacano. Con autorización temporal, la skill puede crear o
87
- ajustar tipos y componentes mediante el entrypoint administrativo oficial; un
88
- fixture activo bloquea la aprobación de producción.
89
+ con el mismo contrato de los componentes. Con autorización temporal y scopes
90
+ explícitos, puede crear o ajustar tipos, componentes, grupos, listas y entradas
91
+ mediante el entrypoint administrativo oficial. Las entradas nacen en borrador;
92
+ publicar exige permiso y aprobación humana. Un fixture activo en producción
93
+ bloquea la aprobación del sitio.
89
94
 
90
95
  El snapshot puede validarse con:
91
96