@bacano/sdk 1.1.10 → 1.1.13

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.
@@ -0,0 +1,401 @@
1
+ # Gestión segura de Content para agentes
2
+
3
+ `@bacano/sdk/content-management` permite que una herramienta administrativa o
4
+ un agente autorizado consulte y mantenga **tipos, componentes, grupos, listas
5
+ de opciones y entradas** de Bacano Content. Cada capacidad requiere un permiso explícito. El acceso no
6
+ autoriza productos, ventas, clientes, configuración general, GraphQL, SQL ni
7
+ Storage.
8
+
9
+ Este contrato es independiente del cliente público del storefront. Debe usarse
10
+ solo desde un proceso local o servidor confiable; nunca desde el navegador ni
11
+ dentro del bundle del sitio web.
12
+
13
+ ## Activar un acceso temporal
14
+
15
+ 1. En Bacano, abre **Sitios web → Contenido → Tipos** o **Componentes**.
16
+ 2. Selecciona **Conectar agente**.
17
+ 3. Elige el alcance, 15, 30 o 60 minutos y genera el acceso.
18
+ 4. Copia el token al entorno local del agente.
19
+
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`: reservado para una fase posterior; no se concede
34
+ mientras la carga administrativa de media no esté implementada.
35
+
36
+ No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
37
+ que se empaqueten con el storefront.
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
+
45
+ ## Crear el cliente
46
+
47
+ ```ts
48
+ import { createContentManagementClient } from "@bacano/sdk/content-management";
49
+
50
+ const content = createContentManagementClient({
51
+ apiUrl: process.env.BACANO_API_URL!,
52
+ accessToken: process.env.BACANO_CONTENT_ACCESS_TOKEN!,
53
+ });
54
+
55
+ const context = await content.getContext();
56
+ console.log(context.companyId, context.scopes, context.expiresAt);
57
+ ```
58
+
59
+ El cliente ofrece:
60
+
61
+ - `getContext()` para confirmar empresa, alcance y vencimiento.
62
+ - `getReferences()` para consultar categorías, subcategorías, listas de opciones
63
+ y unidades de medida válidas.
64
+ - `listTypes()` y `getType(idOrKey)`.
65
+ - `createType(input)` y `updateType(idOrKey, input)`.
66
+ - `inspectLifecycle(idOrKey, operation)`.
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
+
81
+ ## Idioma, nombres y keys
82
+
83
+ Antes de proponer una estructura, resolver su idioma en este orden:
84
+
85
+ 1. El idioma solicitado expresamente por el usuario.
86
+ 2. La convención predominante del proyecto existente.
87
+ 3. El idioma principal del website.
88
+ 4. Si todavía existe una ambigüedad material, preguntar al usuario.
89
+
90
+ Para un website en español, los nombres visibles y las keys nuevas se crean en
91
+ español salvo instrucción contraria. Esta regla aplica a identificadores
92
+ definidos por la empresa; los valores reservados del SDK como `entry`,
93
+ `component`, `draft`, `website_content`, `text` o `repeater` no se traducen ni
94
+ se reemplazan por valores inventados.
95
+
96
+ Usar estas convenciones:
97
+
98
+ | Elemento | Convención | Ejemplo |
99
+ | --- | --- | --- |
100
+ | Nombre visible | Lenguaje natural, con tildes | `Preguntas frecuentes` |
101
+ | Key de tipo, componente, sección o campo | `snake_case`, sin tildes | `preguntas_frecuentes` |
102
+ | Key o slug público de una entrada | `kebab-case`, sin tildes | `preguntas-frecuentes-inicio` |
103
+
104
+ Las keys deben describir el significado del contenido, no detalles accidentales
105
+ de su implementación visual. Preferir `titulo`, `descripcion`,
106
+ `imagen_principal`, `enlace_contacto`, `preguntas`, `planes` o `pasos`. Evitar
107
+ identificadores opacos como `data`, `content`, `items`, `field1`,
108
+ `step1_title` o `component_2` cuando existe un nombre semántico claro.
109
+
110
+ Para elementos repetidos, modelar un repetible y, cuando corresponda, un
111
+ componente semántico. Por ejemplo, usar `pasos` con un componente `paso` que
112
+ contenga `titulo` y `descripcion`, en vez de crear `paso1_titulo`,
113
+ `paso1_descripcion`, `paso2_titulo` y campos sucesivos.
114
+
115
+ Antes de crear algo nuevo:
116
+
117
+ 1. Consultar `listTypes()` y `getReferences()`.
118
+ 2. Reutilizar una estructura solo cuando conserva el mismo significado y la
119
+ misma forma, no únicamente porque algunos campos coinciden.
120
+ 3. Dar un nombre neutral a una estructura reutilizable, por ejemplo
121
+ `seccion_preguntas_frecuentes`.
122
+ 4. Incluir el contexto en el nombre cuando la estructura sea realmente
123
+ exclusiva, por ejemplo `calculadora_eficiencia_inicio`.
124
+ 5. Evitar nombres demasiado genéricos como `contenido` y nombres que quedarían
125
+ engañosos al reutilizarse, como `contenedor_principal_servicios` para una
126
+ página de blog.
127
+
128
+ Una key activa es un contrato con el storefront. Nunca traducir, regenerar ni
129
+ renombrar automáticamente keys existentes. La convención de idioma aplica a
130
+ estructuras nuevas; cambiar una key utilizada requiere tratarlo como una
131
+ migración explícita y presentar su impacto. Tampoco crear sufijos como `_2` o
132
+ `-2` sin investigar primero el identificador existente.
133
+
134
+ Antes de ejecutar `createType()`, presentar al usuario el nombre, key, idioma,
135
+ uso, campos, componentes y decisión de creación o reutilización. Corregir
136
+ mezclas de idiomas y errores ortográficos antes de materializar el blueprint.
137
+
138
+ ## Flujo seguro para un agente
139
+
140
+ 1. Ejecutar `getContext()` y confirmar la empresa con el usuario.
141
+ 2. Ejecutar `listTypes()` y `getReferences()` antes de proponer cambios.
142
+ 3. Reutilizar tipos, componentes, listas y keys existentes cuando correspondan.
143
+ 4. Crear primero los componentes que serán referenciados por otros tipos.
144
+ 5. Crear o actualizar en `draft`.
145
+ 6. Volver a consultar el tipo y presentar un resumen del resultado.
146
+ 7. Activarlo únicamente cuando el usuario haya aprobado la estructura.
147
+ 8. Para archivar, restaurar o eliminar, ejecutar primero
148
+ `inspectLifecycle()` y mostrar dependencias y bloqueos.
149
+ 9. Ejecutar `applyLifecycle()` solo después de una confirmación explícita.
150
+
151
+ `updateType()` reemplaza la definición completa. Antes de usarlo debes obtener
152
+ el tipo con `getType()`, conservar los IDs de campos existentes y enviar todos
153
+ los campos y secciones que deban permanecer. La omisión de un campo se interpreta
154
+ como una eliminación estructural y puede requerir
155
+ `confirmDestructiveChanges: true`.
156
+
157
+ ## Ejemplo: componente y tipo editorial
158
+
159
+ ```ts
160
+ await content.createType({
161
+ key: "boton_llamada_accion",
162
+ name: "Botón de llamada a la acción",
163
+ kind: "component",
164
+ status: "draft",
165
+ fields: [
166
+ {
167
+ key: "texto",
168
+ label: "Texto",
169
+ type: "text",
170
+ required: true,
171
+ sortOrder: 1,
172
+ settings: { isVisible: true, maxLength: 80 },
173
+ },
174
+ {
175
+ key: "enlace",
176
+ label: "Enlace",
177
+ type: "link",
178
+ required: true,
179
+ sortOrder: 2,
180
+ settings: { isVisible: true },
181
+ },
182
+ ],
183
+ });
184
+
185
+ await content.createType({
186
+ key: "hero_inicio",
187
+ name: "Hero del inicio",
188
+ kind: "entry",
189
+ status: "draft",
190
+ usages: ["website_content"],
191
+ sections: [{ key: "contenido", title: "Contenido", sortOrder: 1 }],
192
+ fields: [
193
+ {
194
+ key: "titulo",
195
+ label: "Título",
196
+ type: "text",
197
+ required: true,
198
+ sortOrder: 1,
199
+ sectionKey: "contenido",
200
+ settings: { isVisible: true, maxLength: 120 },
201
+ },
202
+ {
203
+ key: "acciones",
204
+ label: "Acciones",
205
+ type: "repeater",
206
+ required: false,
207
+ sortOrder: 2,
208
+ sectionKey: "contenido",
209
+ settings: { isVisible: true, minRows: 0, maxRows: 3 },
210
+ children: [
211
+ {
212
+ key: "boton",
213
+ label: "Botón",
214
+ type: "component",
215
+ required: true,
216
+ sortOrder: 1,
217
+ componentKey: "boton_llamada_accion",
218
+ settings: { isVisible: true },
219
+ },
220
+ ],
221
+ },
222
+ ],
223
+ });
224
+ ```
225
+
226
+ ## Ajustes de campos
227
+
228
+ Todos los campos aceptan `helpText`, `defaultValue`, `isPrivate` e
229
+ `isVisible`. Usa además los ajustes correspondientes al tipo:
230
+
231
+ | Tipo | Ajustes principales |
232
+ | --- | --- |
233
+ | `text`, `textarea`, `slug` | `minLength`, `maxLength`, `pattern` |
234
+ | `rich_text`, `rich_text_full` | `richTextMode` |
235
+ | `media` | `mediaMultiple`, `acceptedMediaKinds` |
236
+ | `date` | `dateMode`: `date` o `datetime` |
237
+ | `number` | `min`, `max`, `step`, `precision` |
238
+ | `measure` | `measureDimensionCode`, `measureAllowedUnitCodes`, `measurePublicUnitCode` |
239
+ | `select` | `selectSource`, `selectOptions`, `selectSourceKey`, `selectMultiple` |
240
+ | `reference` | `referenceType`, `referenceContentTypeKey`, `referenceMultiple` |
241
+ | `repeater` | `minRows`, `maxRows` y `children` |
242
+ | `component` | `componentKey` |
243
+
244
+ Para una lista administrable usa `selectSource: "option_list"` y una key
245
+ devuelta por `getReferences().optionLists`. Para medidas usa exclusivamente los
246
+ códigos devueltos por `getReferences().measureDimensions`; no inventes unidades.
247
+
248
+ Una ficha técnica usa `usages: ["product_specification"]` y al menos un alcance
249
+ en `specificationScopes`. Sus campos técnicos pueden declarar
250
+ `isFilterable: true` únicamente cuando las reglas de Bacano lo permitan. Los
251
+ otros usos editoriales son `website_content` y `product_additional`.
252
+
253
+ ## Ciclo de vida
254
+
255
+ ```ts
256
+ const preview = await content.inspectLifecycle("hero_inicio", "delete");
257
+
258
+ if (preview.allowed && userConfirmed) {
259
+ await content.applyLifecycle("hero_inicio", "delete");
260
+ }
261
+ ```
262
+
263
+ Nunca ignores `dependencies`, `allowed` o `errors`. Archivar es preferible a
264
+ eliminar cuando existe contenido histórico. El backend aplica las mismas reglas
265
+ de dependencias, keys, componentes, secciones y cambios destructivos que el
266
+ backoffice; el SDK no crea un camino alterno.
267
+
268
+ ## Entradas
269
+
270
+ Antes de crear una entrada, confirma que su tipo existe y que la estructura
271
+ aprobada ya está activa. Las entradas nacen en `draft` salvo que el usuario haya
272
+ concedido explícitamente `content_entries:publish`.
273
+
274
+ ```ts
275
+ const existing = await content.listEntries({
276
+ contentType: "hero_inicio",
277
+ locale: "es-CO",
278
+ limit: 100,
279
+ });
280
+
281
+ const current = existing.entries.find(
282
+ (entry) => entry.key === "hero-principal-inicio",
283
+ );
284
+
285
+ if (!current) {
286
+ await content.createEntry({
287
+ contentType: "hero_inicio",
288
+ key: "hero-principal-inicio",
289
+ name: "Hero principal del inicio",
290
+ locale: "es-CO",
291
+ status: "draft",
292
+ content: {
293
+ titulo: "Encuentra lo mejor para cada etapa",
294
+ descripcion: "Compra productos seleccionados para tu familia.",
295
+ },
296
+ });
297
+ } else {
298
+ await content.updateEntry(current.id, {
299
+ contentType: current.contentType.id,
300
+ key: current.key,
301
+ name: current.name,
302
+ locale: current.locale,
303
+ status: "draft",
304
+ content: {
305
+ ...current.content,
306
+ titulo: "Encuentra lo mejor para cada etapa",
307
+ },
308
+ expectedUpdatedAt: current.updatedAt,
309
+ });
310
+ }
311
+ ```
312
+
313
+ Reglas obligatorias:
314
+
315
+ 1. Inventariar por tipo, key y locale antes de crear; nunca fusionar por nombre.
316
+ 2. Usar `expectedUpdatedAt` al actualizar. Un conflicto exige volver a consultar
317
+ y presentar el cambio; no se sobrescribe silenciosamente.
318
+ 3. Enviar el documento completo que debe permanecer. `updateEntry()` reemplaza
319
+ el contenido de la entrada después de aplicar las validaciones oficiales.
320
+ 4. Reutilizar únicamente `fileId` ya existentes y pertenecientes a la misma
321
+ empresa. Este acceso no sube media ni acepta URLs arbitrarias.
322
+ 5. Rich text usa el JSON controlado por Bacano, nunca HTML crudo.
323
+ 6. Publicar, retirar o modificar una entrada publicada requiere
324
+ `content_entries:publish` y aprobación humana explícita.
325
+ 7. Inspeccionar el ciclo de vida antes de archivar, restaurar o eliminar.
326
+
327
+ La API reutiliza el mismo servicio de guardado del backoffice: campos
328
+ obligatorios, componentes anidados, repetibles, listas, referencias, rich text,
329
+ media y pertenencia a la empresa se validan de forma autoritativa.
330
+
331
+ ## Grupos y listas de opciones
332
+
333
+ Los grupos organizan entradas y usan el scope `content_entries:manage`. Las
334
+ listas proporcionan valores controlados para campos `select`, usan
335
+ `content_types:manage` y conservan sus relaciones padre-hijo.
336
+
337
+ ```ts
338
+ const group = await content.createGroup({
339
+ key: "pagina-principal",
340
+ name: "Página principal",
341
+ status: "active",
342
+ });
343
+
344
+ const materials = await content.createOptionList({
345
+ key: "materiales",
346
+ name: "Materiales",
347
+ status: "draft",
348
+ isExtensible: false,
349
+ items: [
350
+ { value: "algodon", label: "Algodón", sortOrder: 1 },
351
+ { value: "lino", label: "Lino", sortOrder: 2 },
352
+ ],
353
+ });
354
+
355
+ await content.updateOptionList(materials.id, {
356
+ key: materials.key,
357
+ name: materials.name,
358
+ status: "active",
359
+ isExtensible: materials.isExtensible,
360
+ items: materials.items.map((item) => ({
361
+ id: item.id,
362
+ value: item.value,
363
+ label: item.label,
364
+ parentItemId: item.parentItemId,
365
+ sortOrder: item.sortOrder,
366
+ isActive: item.isActive,
367
+ metadata: item.metadata,
368
+ })),
369
+ });
370
+ ```
371
+
372
+ `updateGroup()` y `updateOptionList()` reciben el documento completo que debe
373
+ permanecer. Las keys existentes son inmutables. Para retirar elementos,
374
+ archivar, restaurar o eliminar, primero usa la operación `inspect*Lifecycle()`.
375
+ El backend bloquea desactivar o retirar opciones que todavía usan listas
376
+ dependientes y exige `confirmDestructiveChanges` cuando corresponde; nunca
377
+ omitas opciones existentes por accidente en una actualización.
378
+
379
+ ## Seguridad y auditoría
380
+
381
+ - El `companyId` y el usuario provienen del token; no se aceptan desde el body.
382
+ - Cada consulta y mutación está filtrada por la empresa firmada.
383
+ - UUIDs o keys de otra empresa responden como inexistentes y no se modifican.
384
+ - Las concesiones pueden revocarse individualmente desde Bacano.
385
+ - Emisión, revocación y mutaciones quedan en una auditoría inmutable sin guardar
386
+ el bearer token ni el documento completo.
387
+ - Las respuestas del endpoint usan `no-store`, límites de tamaño y rate limit.
388
+ - Los tokens v2 no son compatibles con tokens anteriores y tienen como máximo
389
+ 60 minutos de vigencia.
390
+
391
+ ## Límites de esta versión
392
+
393
+ - No carga archivos ni crea media; puede relacionar media existente validada.
394
+ - No funciona desde el cliente público del storefront.
395
+ - No acepta GraphQL directo ni credenciales administrativas permanentes.
396
+ - No previsualiza borradores desde el storefront público.
397
+
398
+ El resultado esperado del flujo Figma es: el agente propone el blueprint, el
399
+ usuario habilita únicamente los scopes necesarios, el agente crea o ajusta la
400
+ estructura y prepara entradas en borrador, y una persona revisa antes de
401
+ publicar.
package/README.md CHANGED
@@ -21,6 +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 Bacano Content para agentes
25
+
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:
31
+
32
+ ```ts
33
+ import { createContentManagementClient } from "@bacano/sdk/content-management";
34
+ ```
35
+
36
+ Lee [`CONTENT_TYPE_MANAGEMENT.md`](./CONTENT_TYPE_MANAGEMENT.md) antes de usarlo.
37
+ La guía contiene el flujo de autorización, contratos de campos y entradas,
38
+ estados, referencias, cambios destructivos y límites de seguridad. La carga de
39
+ media continúa en el backoffice hasta que exista un contrato administrativo
40
+ oficial para ella.
41
+
24
42
  ### Auditoría del storefront
25
43
 
26
44
  El SDK incluye la skill oficial `bacano-web`. Instalarla al iniciar un
@@ -47,8 +65,8 @@ el usuario o agente invoca la skill.
47
65
  ### Implementación desde Figma
48
66
 
49
67
  El SDK también incluye `bacano-figma-web`, la skill oficial para convertir un
50
- diseño Figma en una web informativa o tienda que conserve el cascarón, Static
51
- First y contratos públicos de Bacano:
68
+ diseño Figma en una web informativa, tienda o sitio híbrido que conserve el
69
+ cascarón, Static First y contratos públicos de Bacano:
52
70
 
53
71
  ```bash
54
72
  pnpm exec bacano-figma-web install --agent all
@@ -67,8 +85,11 @@ la presencia del token REST. El OAuth del MCP remoto debe confirmarse dentro del
67
85
  agente con las herramientas de Figma. La extracción se conserva como snapshot
68
86
  normalizado. Cuando el equipo de Content y desarrollo trabajan en tiempos
69
87
  distintos, la skill genera un blueprint de entrega y permite fixtures locales
70
- con el mismo contrato de los componentes. Los tipos y entradas reales se crean
71
- desde Bacano; un fixture activo bloquea la aprobación de producción.
88
+ con el mismo contrato de los componentes. Con autorización temporal y scopes
89
+ explícitos, puede crear o ajustar tipos, componentes, grupos, listas y entradas
90
+ mediante el entrypoint administrativo oficial. Las entradas nacen en borrador;
91
+ publicar exige permiso y aprobación humana. Un fixture activo en producción
92
+ bloquea la aprobación del sitio.
72
93
 
73
94
  El snapshot puede validarse con:
74
95