ponch-mcp-server 1.0.88 → 1.0.90

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.
package/dist/index.js CHANGED
@@ -171,29 +171,36 @@ var Session = class {
171
171
  this.context.brandId = brandId;
172
172
  }
173
173
  /**
174
- * Cambia la brand activa SIN cambiar tenant. Disponible para cualquier
175
- * user con multi-brand (agency mode). DEUDA 3 sesión 211.1 H/I verifica
176
- * que brandId esté en la lista de brands accesibles del user (cargada
177
- * en connect_account).
174
+ * Cambia la brand activa SIN cambiar tenant. Disponible para:
175
+ * - User Modo B con multi-brand (brands.length > 1)agency mode.
176
+ * - Super_admin Modo A (canSwitchTenant=true) service account
177
+ * puede operar cualquier brand dentro del tenant activo seteado
178
+ * previamente vía set_context.
179
+ *
180
+ * DEUDA 3 sesión 211.1 H/I + fix J post-smoke 1.0.88.
178
181
  *
179
182
  * Lanza si:
180
- * - El user no tiene multi-brand (brands.length <= 1).
181
- * - El brandId solicitado NO está en la lista del user (defensa
182
- * extra contra cross-brand attack el helper también valida que
183
- * exista en Firestore antes de invocar esto).
183
+ * - El user no es super_admin Y no tiene multi-brand.
184
+ * - El brandId NO está en lista del user (defensa cross-brand Modo B).
185
+ * Para super_admin Modo A se delega validación al helper (Firestore
186
+ * existence check) no hay "lista accesible" porque tiene acceso
187
+ * total al tenant.
184
188
  */
185
189
  setBrand(brandId) {
186
190
  const brands = this._authContext.brands ?? [];
187
- if (brands.length <= 1) {
191
+ const isSuperAdmin = this._authContext.canSwitchTenant;
192
+ if (!isSuperAdmin && brands.length <= 1) {
188
193
  throw new Error(
189
- `select_brand requires multi-brand access. This user has access to ${brands.length} brand(s).`
194
+ `select_brand requires multi-brand access or super_admin. This user has access to ${brands.length} brand(s) and is not super_admin.`
190
195
  );
191
196
  }
192
- const allowed = brands.some((b) => b.id === brandId);
193
- if (!allowed) {
194
- throw new Error(
195
- `Brand "${brandId}" is not in the user's accessible brands list [${brands.map((b) => b.id).join(", ")}].`
196
- );
197
+ if (!isSuperAdmin) {
198
+ const allowed = brands.some((b) => b.id === brandId);
199
+ if (!allowed) {
200
+ throw new Error(
201
+ `Brand "${brandId}" is not in the user's accessible brands list [${brands.map((b) => b.id).join(", ")}].`
202
+ );
203
+ }
197
204
  }
198
205
  this.context.brandId = brandId;
199
206
  this._authContext.brandId = brandId;
@@ -1525,7 +1532,7 @@ var ORIGENES_CONTENIDO = [
1525
1532
  "imported"
1526
1533
  // sync Shopify/WordPress
1527
1534
  ];
1528
- var ESTADO_CONTENIDO2 = {
1535
+ var ESTADO_CONTENIDO = {
1529
1536
  BORRADOR: "borrador",
1530
1537
  GENERADO: "generado",
1531
1538
  PENDIENTE_APROBACION: "pendiente_aprobacion",
@@ -1723,7 +1730,7 @@ var ESTADOS_FOTO = [
1723
1730
  var ESTRATEGIAS_EDICION = ["gemini_edit", "tal_cual"];
1724
1731
  var FotoEstadoEnum = import_zod13.z.enum(ESTADOS_FOTO);
1725
1732
  var FotoEstrategiaEdicionEnum = import_zod13.z.enum(ESTRATEGIAS_EDICION);
1726
- var ESTADO_FOTO2 = {
1733
+ var ESTADO_FOTO = {
1727
1734
  NUEVA: "nueva",
1728
1735
  PROCESANDO: "procesando",
1729
1736
  EDITADA: "editada",
@@ -9967,7 +9974,10 @@ function wrapWithContract(contract, helper, options = {}) {
9967
9974
  state: "denied_role"
9968
9975
  };
9969
9976
  }
9970
- const parseResult = contract.paramsSchema.safeParse(input);
9977
+ const cleanInput = input && typeof input === "object" && !Array.isArray(input) ? Object.fromEntries(
9978
+ Object.entries(input).filter(([, v]) => v !== null)
9979
+ ) : input;
9980
+ const parseResult = contract.paramsSchema.safeParse(cleanInput);
9971
9981
  if (!parseResult.success) {
9972
9982
  const validationErrors = parseResult.error.issues.map((i) => {
9973
9983
  const issue = i;
@@ -11869,7 +11879,7 @@ async function calendarSlotUpdater(input) {
11869
11879
  const contenidoQuery = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("calendarioItemRef", "==", oldContenidoRef).limit(10).get();
11870
11880
  contenidosADescartar = contenidoQuery.docs.filter((c) => {
11871
11881
  const data = c.data();
11872
- return data.estado !== "descartado" && data.estado !== ESTADO_CONTENIDO2.PUBLICADO;
11882
+ return data.estado !== "descartado" && data.estado !== ESTADO_CONTENIDO.PUBLICADO;
11873
11883
  });
11874
11884
  }
11875
11885
  if (accionContenidoExistente === "nuevo_slot") {
@@ -12784,7 +12794,7 @@ async function contenidoApprover(input) {
12784
12794
  }
12785
12795
  const data = snap.data();
12786
12796
  const estadoActual = typeof data.estado === "string" ? data.estado : "";
12787
- if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO2.APROBADO)) {
12797
+ if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO.APROBADO)) {
12788
12798
  return {
12789
12799
  ok: false,
12790
12800
  code: "INVALID_STATE_TRANSITION",
@@ -12792,7 +12802,7 @@ async function contenidoApprover(input) {
12792
12802
  };
12793
12803
  }
12794
12804
  await ref.update({
12795
- estado: ESTADO_CONTENIDO2.APROBADO,
12805
+ estado: ESTADO_CONTENIDO.APROBADO,
12796
12806
  aprobadoAt: import_firebase_admin8.firestore.FieldValue.serverTimestamp(),
12797
12807
  aprobadoPorId: actor.uid,
12798
12808
  aprobadoPorNombre: actor.nombre,
@@ -12803,7 +12813,7 @@ async function contenidoApprover(input) {
12803
12813
  ok: true,
12804
12814
  contenidoId,
12805
12815
  estadoAnterior: estadoActual,
12806
- nuevoEstado: ESTADO_CONTENIDO2.APROBADO
12816
+ nuevoEstado: ESTADO_CONTENIDO.APROBADO
12807
12817
  };
12808
12818
  }
12809
12819
  var ParamsSchema12 = import_zod47.z.object({
@@ -12873,7 +12883,7 @@ async function contenidoRejecter(input) {
12873
12883
  }
12874
12884
  const data = snap.data();
12875
12885
  const estadoActual = typeof data.estado === "string" ? data.estado : "";
12876
- if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO2.RECHAZADO)) {
12886
+ if (!esTransicionValida(estadoActual, ESTADO_CONTENIDO.RECHAZADO)) {
12877
12887
  return {
12878
12888
  ok: false,
12879
12889
  code: "INVALID_STATE_TRANSITION",
@@ -12881,7 +12891,7 @@ async function contenidoRejecter(input) {
12881
12891
  };
12882
12892
  }
12883
12893
  await ref.update({
12884
- estado: ESTADO_CONTENIDO2.RECHAZADO,
12894
+ estado: ESTADO_CONTENIDO.RECHAZADO,
12885
12895
  rechazadoAt: import_firebase_admin9.firestore.FieldValue.serverTimestamp(),
12886
12896
  rechazadoMotivo: motivo,
12887
12897
  rechazadoPorId: actor.uid,
@@ -12893,7 +12903,7 @@ async function contenidoRejecter(input) {
12893
12903
  ok: true,
12894
12904
  contenidoId,
12895
12905
  estadoAnterior: estadoActual,
12896
- nuevoEstado: ESTADO_CONTENIDO2.RECHAZADO,
12906
+ nuevoEstado: ESTADO_CONTENIDO.RECHAZADO,
12897
12907
  motivo
12898
12908
  };
12899
12909
  }
@@ -12958,7 +12968,7 @@ var contenidoRejecterContract = MartinContractSchema.parse(
12958
12968
  rawContract13
12959
12969
  );
12960
12970
  async function photoBriefingWriter(input) {
12961
- const { db, tenantId, brandId, semana, necesidades } = input;
12971
+ const { db, tenantId, brandId, semana, necesidades, actor } = input;
12962
12972
  const docId = `${tenantId}_${brandId}_${semana}`;
12963
12973
  const ref = db.collection("tenants").doc(tenantId).collection("marketing_fotobriefings").doc(docId);
12964
12974
  const snap = await ref.get();
@@ -12998,9 +13008,23 @@ async function photoBriefingWriter(input) {
12998
13008
  estado,
12999
13009
  generadoEn: existing?.generadoEn ?? null
13000
13010
  });
13011
+ const isCreate = !snap.exists;
13012
+ const actorFields = {
13013
+ actualizadoPorId: actor.uid,
13014
+ actualizadoPorNombre: actor.nombre,
13015
+ actualizadoPorClient: actor.clientType,
13016
+ actualizadoPorClientMetadata: actor.clientMetadata ?? null
13017
+ };
13018
+ if (isCreate) {
13019
+ actorFields.creadoPorId = actor.uid;
13020
+ actorFields.creadoPorNombre = actor.nombre;
13021
+ actorFields.creadoPorClient = actor.clientType;
13022
+ actorFields.creadoPorClientMetadata = actor.clientMetadata ?? null;
13023
+ }
13001
13024
  await ref.set(
13002
13025
  {
13003
13026
  ...doc,
13027
+ ...actorFields,
13004
13028
  generadoEn: existing?.generadoEn ?? import_firebase_admin10.firestore.FieldValue.serverTimestamp(),
13005
13029
  fechaActualizacion: import_firebase_admin10.firestore.FieldValue.serverTimestamp()
13006
13030
  },
@@ -13027,7 +13051,13 @@ var ParamsSchema14 = import_zod49.z.object({
13027
13051
  tenantId: import_zod49.z.string().min(1).describe('Tenant identifier. Example: "your-tenant-id".'),
13028
13052
  brandId: import_zod49.z.string().min(1).describe('Brand identifier within the tenant. Example: "your-brand-id".'),
13029
13053
  semana: import_zod49.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "semana debe ser ISO YYYY-MM-DD del lunes").describe('Week identifier (ISO date of Monday). Example: "2026-05-11".'),
13030
- necesidades: import_zod49.z.array(NecesidadInputSchema).min(1).describe("Array of photo needs (at least 1). Each is merged by tema.")
13054
+ necesidades: import_zod49.z.array(NecesidadInputSchema).min(1).describe("Array of photo needs (at least 1). Each is merged by tema."),
13055
+ actor: import_zod49.z.object({
13056
+ uid: import_zod49.z.string().min(1).describe('User id creating/updating the briefing. Example: "your-user-uid".'),
13057
+ nombre: import_zod49.z.string().min(1).describe('Human-readable user name. Example: "Daniel Gonz\xE1lez".'),
13058
+ clientType: import_zod49.z.string().min(1).describe('Client surface origin. Example: one of "mcp_client" | "martin" | "web" | "admin".'),
13059
+ clientMetadata: import_zod49.z.record(import_zod49.z.string(), import_zod49.z.unknown()).nullable().optional().describe("Optional metadata about the client (session id, device, etc.).")
13060
+ }).describe("Actor (user) creating/updating the briefing \u2014 extracted by the server.tool from ctx.user (\xA79.2).")
13031
13061
  });
13032
13062
  var OutputSchema14 = import_zod49.z.object({
13033
13063
  ok: import_zod49.z.literal(true),
@@ -13178,7 +13208,7 @@ async function photoDescarter(input) {
13178
13208
  }
13179
13209
  const data = snap.data();
13180
13210
  const estadoActual = typeof data.estado === "string" ? data.estado : "";
13181
- if (!validarTransicionFoto(estadoActual, ESTADO_FOTO2.DESCARTADA)) {
13211
+ if (!validarTransicionFoto(estadoActual, ESTADO_FOTO.DESCARTADA)) {
13182
13212
  return {
13183
13213
  ok: false,
13184
13214
  code: "INVALID_STATE_TRANSITION",
@@ -13186,7 +13216,7 @@ async function photoDescarter(input) {
13186
13216
  };
13187
13217
  }
13188
13218
  await ref.update({
13189
- estado: ESTADO_FOTO2.DESCARTADA,
13219
+ estado: ESTADO_FOTO.DESCARTADA,
13190
13220
  descartadaAt: import_firebase_admin11.firestore.FieldValue.serverTimestamp(),
13191
13221
  descartadaPorId: actor.uid,
13192
13222
  descartadaPorNombre: actor.nombre,
@@ -13197,7 +13227,7 @@ async function photoDescarter(input) {
13197
13227
  ok: true,
13198
13228
  fotoId,
13199
13229
  estadoAnterior: estadoActual,
13200
- nuevoEstado: ESTADO_FOTO2.DESCARTADA
13230
+ nuevoEstado: ESTADO_FOTO.DESCARTADA
13201
13231
  };
13202
13232
  }
13203
13233
  var ParamsSchema16 = import_zod51.z.object({
@@ -13268,7 +13298,7 @@ async function photoEditadaMarker(input) {
13268
13298
  const data = snap.data();
13269
13299
  const estadoActual = typeof data.estado === "string" ? data.estado : "";
13270
13300
  const archivoOriginal = typeof data.archivoOriginal === "string" ? data.archivoOriginal : null;
13271
- if (!validarTransicionFoto(estadoActual, ESTADO_FOTO2.EDITADA)) {
13301
+ if (!validarTransicionFoto(estadoActual, ESTADO_FOTO.EDITADA)) {
13272
13302
  return {
13273
13303
  ok: false,
13274
13304
  code: "INVALID_STATE_TRANSITION",
@@ -13279,7 +13309,7 @@ async function photoEditadaMarker(input) {
13279
13309
  return { ok: false, code: "FOTO_SIN_ARCHIVO_ORIGINAL", estadoActual };
13280
13310
  }
13281
13311
  await ref.update({
13282
- estado: ESTADO_FOTO2.EDITADA,
13312
+ estado: ESTADO_FOTO.EDITADA,
13283
13313
  archivoEditado: archivoOriginal,
13284
13314
  fechaEdicion: import_firebase_admin12.firestore.FieldValue.serverTimestamp(),
13285
13315
  marcadaEditadaPorId: actor.uid,
@@ -13291,7 +13321,7 @@ async function photoEditadaMarker(input) {
13291
13321
  ok: true,
13292
13322
  fotoId,
13293
13323
  estadoAnterior: estadoActual,
13294
- nuevoEstado: ESTADO_FOTO2.EDITADA,
13324
+ nuevoEstado: ESTADO_FOTO.EDITADA,
13295
13325
  archivoEditado: archivoOriginal
13296
13326
  };
13297
13327
  }
@@ -13444,7 +13474,7 @@ async function photoAssigner(input) {
13444
13474
  return { ok: false, error: `Foto ${fotoId} no encontrada`, code: "PHOTO_NOT_FOUND" };
13445
13475
  }
13446
13476
  const foto = fotoQuery.docs[0].data();
13447
- if (foto.estado !== ESTADO_FOTO2.EDITADA && foto.estado !== "usada") {
13477
+ if (foto.estado !== ESTADO_FOTO.EDITADA && foto.estado !== "usada") {
13448
13478
  return {
13449
13479
  ok: false,
13450
13480
  error: `Foto en estado "${foto.estado}", debe ser "editada"`,
@@ -13861,7 +13891,7 @@ async function marketingPlanBuilder(input) {
13861
13891
  }
13862
13892
  const productosQ = await db.collection("productos").where("tenantId", "==", tenantId).limit(100).get();
13863
13893
  const productos = productosQ.docs.map((d) => d.data());
13864
- const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO2.PUBLICADO).limit(20).get();
13894
+ const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(20).get();
13865
13895
  const historial = histQ.docs.map((d) => d.data());
13866
13896
  const lastImportId = await resolveLastImportId(db, tenantId, brandId);
13867
13897
  let colecciones = [];
@@ -14182,7 +14212,7 @@ async function contenidoWriter(input) {
14182
14212
  tipo: tipo ?? "post",
14183
14213
  keyword: keyword ?? null,
14184
14214
  languageCode,
14185
- estado: ESTADO_CONTENIDO2.PENDIENTE_APROBACION,
14215
+ estado: ESTADO_CONTENIDO.PENDIENTE_APROBACION,
14186
14216
  fotoId: fotoId ?? null,
14187
14217
  mediaUrl: null,
14188
14218
  calendarioItemRef: calendarioItemRef ?? null,
@@ -14308,7 +14338,7 @@ async function contenidoWriter(input) {
14308
14338
  return {
14309
14339
  ok: true,
14310
14340
  contenidoId: id,
14311
- estado: ESTADO_CONTENIDO2.PENDIENTE_APROBACION,
14341
+ estado: ESTADO_CONTENIDO.PENDIENTE_APROBACION,
14312
14342
  plataforma,
14313
14343
  descartados,
14314
14344
  pipelineLinked
@@ -14542,7 +14572,7 @@ async function weeklyContentBuilder(input) {
14542
14572
  }
14543
14573
  };
14544
14574
  }
14545
- const fotosQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO2.EDITADA).limit(20).get();
14575
+ const fotosQ = await db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO.EDITADA).limit(20).get();
14546
14576
  const fotosDisponibles = fotosQ.docs.map((d) => d.data());
14547
14577
  const histQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).limit(20).get();
14548
14578
  const historial = histQ.docs.map((d) => d.data());
@@ -14552,7 +14582,7 @@ async function weeklyContentBuilder(input) {
14552
14582
  const blogStrategy = brand.blogStrategy;
14553
14583
  const blogs = blogStrategy?.blogs ?? [];
14554
14584
  const idiomas = brand.idiomas;
14555
- const articulosQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("plataforma", "==", "shopify_blog").where("estado", "==", ESTADO_CONTENIDO2.PUBLICADO).limit(20).get();
14585
+ const articulosQ = await db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("plataforma", "==", "shopify_blog").where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(20).get();
14556
14586
  const articulosExistentes = articulosQ.docs.map(
14557
14587
  (d) => d.data()
14558
14588
  );
@@ -16108,8 +16138,8 @@ async function buildTenantContext(db, tenantId, brandId) {
16108
16138
  if (!configSnap.exists) return "";
16109
16139
  const brand = configSnap.data();
16110
16140
  const [fotosQ, contenidoQ, productosQ] = await Promise.all([
16111
- db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO2.EDITADA).limit(10).get(),
16112
- db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO2.PUBLICADO).limit(10).get(),
16141
+ db.collection("tenants").doc(tenantId).collection("marketing_fotos").where("brandId", "==", brandId).where("estado", "==", ESTADO_FOTO.EDITADA).limit(10).get(),
16142
+ db.collection("tenants").doc(tenantId).collection("marketing_contenido").where("brandId", "==", brandId).where("estado", "==", ESTADO_CONTENIDO.PUBLICADO).limit(10).get(),
16113
16143
  db.collection("productos").where("tenantId", "==", tenantId).limit(50).get()
16114
16144
  ]);
16115
16145
  const fotosEditadas = fotosQ.docs.map((d) => d.data());
@@ -16576,7 +16606,9 @@ function registerContextTools(server, session) {
16576
16606
  }
16577
16607
  );
16578
16608
  }
16579
- if (session.brands && session.brands.length > 1) {
16609
+ const isMultiBrand = session.brands && session.brands.length > 1;
16610
+ const isSuperAdmin = session.canSwitchTenant;
16611
+ if (isMultiBrand || isSuperAdmin) {
16580
16612
  server.tool(
16581
16613
  "select_brand",
16582
16614
  "Switch the active brand within your tenant. Use when you have multi-brand access (agency mode) and want to work with a different brand without passing brandId on every tool call. Does NOT change tenant.",
@@ -17490,11 +17522,17 @@ IMPORTANT \u2014 'razon' field: the tenant sees this in the app. Write in friend
17490
17522
  const tenantId = session.requireTenant();
17491
17523
  const brandId = inputBrandId ?? session.requireBrand();
17492
17524
  const ctx = await buildContext(session, brandId);
17525
+ const actor = {
17526
+ uid: ctx.user.uid,
17527
+ nombre: ctx.user.nombre ?? ctx.user.uid,
17528
+ clientType: ctx.user.clientType ?? "mcp_client",
17529
+ clientMetadata: ctx.user.clientMetadata ?? null
17530
+ };
17493
17531
  const result = await dispatchWithContract({
17494
17532
  contract: photoBriefingWriterContract,
17495
17533
  helper: photoBriefingWriter,
17496
17534
  callable: callPhotoBriefingWriter,
17497
- input: { tenantId, brandId, semana, necesidades },
17535
+ input: { tenantId, brandId, semana, necesidades, actor },
17498
17536
  ctx
17499
17537
  });
17500
17538
  let payload;