ponch-mcp-server 1.0.73 → 1.0.74

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
@@ -3129,7 +3129,38 @@ var MartinContractSchema = import_zod28.z.object({
3129
3129
  extractChanges: ExtractChangesSchema.optional(),
3130
3130
  // Governance
3131
3131
  quotasConsumed: import_zod28.z.array(import_zod28.z.string()),
3132
- requiredRoles: import_zod28.z.array(import_zod28.z.string()).min(1),
3132
+ // ── PermissionScope (HITO 6 A6) ────────────────────────────
3133
+ // Cada contract declara EXPLÍCITAMENTE su scope de acceso.
3134
+ // Sin paths legacy. Sin opcionales. Un solo modelo.
3135
+ //
3136
+ // 'module' — Tool de un módulo del negocio (marketing, compras, etc.)
3137
+ // Requiere permissionKey + permissionAction. El wrapper
3138
+ // consulta permisosPorModulo del rol del usuario.
3139
+ //
3140
+ // 'self' — Tool sobre datos del propio usuario (memorias, rutinas
3141
+ // personales con Martin). El wrapper solo verifica que
3142
+ // el usuario esté logueado en el tenant. El helper
3143
+ // individual valida que el doc pertenezca al uid del
3144
+ // usuario (doc.uid === ctx.user.uid).
3145
+ //
3146
+ // 'saas' — Tool del admin del SaaS (deleteTenant, suspendTenant).
3147
+ // Solo super_admin pasa. Validación tenant-ownership la
3148
+ // hace el helper internamente.
3149
+ permissionScope: import_zod28.z.enum(["module", "self", "saas"]),
3150
+ /**
3151
+ * Key del módulo. Solo si scope === 'module'.
3152
+ * Ejemplos: 'marketing', 'mural', 'analytics', 'compras'.
3153
+ * Coincide con keys que el sistema ya usa en
3154
+ * `configuracion/{tenantId}_roles.{roleId}.permisosPorModulo`.
3155
+ */
3156
+ permissionKey: import_zod28.z.string().min(1).optional(),
3157
+ /**
3158
+ * Nivel mínimo requerido. Solo si scope === 'module'.
3159
+ * - 'ver' → lectura
3160
+ * - 'editar' → mutación reversible
3161
+ * - 'completo' → destructivos / publicación externa
3162
+ */
3163
+ permissionAction: import_zod28.z.enum(["ver", "editar", "completo"]).optional(),
3133
3164
  sideEffects: import_zod28.z.array(SideEffectEnum),
3134
3165
  // Disabled state declaration (HITO 6).
3135
3166
  // Si el helper PUEDE retornar { disabled: true, code }, el author
@@ -3174,6 +3205,37 @@ var MartinContractSchema = import_zod28.z.object({
3174
3205
  path: ["requiresConfirmation"]
3175
3206
  });
3176
3207
  }
3208
+ if (contract.permissionScope === "module") {
3209
+ if (!contract.permissionKey) {
3210
+ ctx.addIssue({
3211
+ code: "custom",
3212
+ message: `Contract "${contract.name}" scope='module' requiere permissionKey.`,
3213
+ path: ["permissionKey"]
3214
+ });
3215
+ }
3216
+ if (!contract.permissionAction) {
3217
+ ctx.addIssue({
3218
+ code: "custom",
3219
+ message: `Contract "${contract.name}" scope='module' requiere permissionAction.`,
3220
+ path: ["permissionAction"]
3221
+ });
3222
+ }
3223
+ } else {
3224
+ if (contract.permissionKey) {
3225
+ ctx.addIssue({
3226
+ code: "custom",
3227
+ message: `Contract "${contract.name}" scope='${contract.permissionScope}' no debe declarar permissionKey (solo aplica a scope='module').`,
3228
+ path: ["permissionKey"]
3229
+ });
3230
+ }
3231
+ if (contract.permissionAction) {
3232
+ ctx.addIssue({
3233
+ code: "custom",
3234
+ message: `Contract "${contract.name}" scope='${contract.permissionScope}' no debe declarar permissionAction (solo aplica a scope='module').`,
3235
+ path: ["permissionAction"]
3236
+ });
3237
+ }
3238
+ }
3177
3239
  });
3178
3240
  var ParamsSchema = import_zod27.z.object({
3179
3241
  tenantId: import_zod27.z.string().min(1),
@@ -3224,7 +3286,9 @@ var rawContract = {
3224
3286
  after: output.ok ? { plan: input.plan } : null
3225
3287
  }),
3226
3288
  quotasConsumed: [],
3227
- requiredRoles: ["admin", "encargado"],
3289
+ permissionScope: "module",
3290
+ permissionKey: "marketing",
3291
+ permissionAction: "editar",
3228
3292
  sideEffects: ["writes_firestore", "updates_brand_config"]
3229
3293
  };
3230
3294
  var planWriterSaveContract = MartinContractSchema.parse(
@@ -3798,7 +3862,9 @@ var rawContract2 = {
3798
3862
  } : null
3799
3863
  }),
3800
3864
  quotasConsumed: [],
3801
- requiredRoles: ["admin", "encargado"],
3865
+ permissionScope: "module",
3866
+ permissionKey: "marketing",
3867
+ permissionAction: "editar",
3802
3868
  sideEffects: ["writes_firestore", "updates_calendar_slot"]
3803
3869
  };
3804
3870
  var calendarSlotUpdaterContract = MartinContractSchema.parse(
@@ -3856,7 +3922,9 @@ var rawContract3 = {
3856
3922
  // AUDITA SIEMPRE — regla A5. Lectura no muta, changes son null/null.
3857
3923
  auditAction: "marketing.calendario.leer",
3858
3924
  quotasConsumed: [],
3859
- requiredRoles: ["admin", "encargado", "empleado"],
3925
+ permissionScope: "module",
3926
+ permissionKey: "marketing",
3927
+ permissionAction: "ver",
3860
3928
  sideEffects: ["reads_firestore"]
3861
3929
  };
3862
3930
  var getCalendarContract = MartinContractSchema.parse(
@@ -4191,7 +4259,9 @@ var rawContract4 = {
4191
4259
  auditAction: "marketing.brand_brief.preparar",
4192
4260
  // No extractTargetPath/extractChanges — no es writer.
4193
4261
  quotasConsumed: [],
4194
- requiredRoles: ["admin", "encargado"],
4262
+ permissionScope: "module",
4263
+ permissionKey: "marketing",
4264
+ permissionAction: "ver",
4195
4265
  sideEffects: ["reads_firestore"]
4196
4266
  };
4197
4267
  var brandBriefBuilderContract = MartinContractSchema.parse(
@@ -6259,25 +6329,68 @@ function getWrapperMessage(key, locale) {
6259
6329
  }
6260
6330
  return msg;
6261
6331
  }
6262
- function hasRequiredRole(userRol, required) {
6263
- if (userRol === "super_admin") return true;
6264
- if (required.includes("*")) return true;
6265
- return required.includes(userRol);
6332
+ var NIVELES_ORDER = ["ninguno", "ver", "editar", "completo"];
6333
+ function nivelAlcanza(nivel, accion) {
6334
+ return NIVELES_ORDER.indexOf(nivel) >= NIVELES_ORDER.indexOf(accion);
6266
6335
  }
6267
- function wrapWithContract(contract, helper) {
6336
+ function wrapWithContract(contract, helper, options = {}) {
6268
6337
  return async function wrappedTool(input, ctx) {
6269
6338
  const startMs = Date.now();
6270
6339
  const locale = ctx.user.idiomaPreferido;
6271
- if (!hasRequiredRole(ctx.user.rol, contract.requiredRoles)) {
6340
+ let accesoOk = false;
6341
+ let reqDesc = "";
6342
+ switch (contract.permissionScope) {
6343
+ case "module": {
6344
+ if (!contract.permissionKey || !contract.permissionAction) {
6345
+ throw new Error(
6346
+ `Wrapper: contract "${contract.name}" scope='module' sin permissionKey/permissionAction. Schema debi\xF3 validar esto.`
6347
+ );
6348
+ }
6349
+ if (!options.permissionResolver) {
6350
+ throw new Error(
6351
+ `Wrapper: contract "${contract.name}" scope='module' pero el caller no inyect\xF3 permissionResolver. Pasarlo en options.`
6352
+ );
6353
+ }
6354
+ reqDesc = `module:${contract.permissionKey}:${contract.permissionAction}`;
6355
+ if (ctx.user.rol === "super_admin") {
6356
+ accesoOk = true;
6357
+ break;
6358
+ }
6359
+ const nivel = await options.permissionResolver({
6360
+ tenantId: ctx.tenantId,
6361
+ userRol: ctx.user.rol,
6362
+ modulo: contract.permissionKey
6363
+ });
6364
+ accesoOk = nivelAlcanza(nivel, contract.permissionAction);
6365
+ break;
6366
+ }
6367
+ case "self": {
6368
+ reqDesc = "self";
6369
+ accesoOk = !!ctx.user.uid && ctx.user.uid !== "";
6370
+ break;
6371
+ }
6372
+ case "saas": {
6373
+ reqDesc = "saas";
6374
+ accesoOk = ctx.user.rol === "super_admin";
6375
+ break;
6376
+ }
6377
+ default: {
6378
+ const _exhaustive = contract.permissionScope;
6379
+ throw new Error(
6380
+ `Wrapper: contract "${contract.name}" permissionScope inv\xE1lido: ${String(_exhaustive)}.`
6381
+ );
6382
+ }
6383
+ }
6384
+ if (!accesoOk) {
6272
6385
  await writeAuditLog({
6273
6386
  tenantId: ctx.tenantId,
6274
6387
  brandId: ctx.brandId ?? null,
6275
6388
  actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
6276
6389
  action: contract.auditAction,
6277
- motivo: `Acceso denegado \u2014 rol '${ctx.user.rol}' no autorizado`,
6390
+ motivo: `Acceso denegado \u2014 rol '${ctx.user.rol}' sin permiso para ${reqDesc}`,
6278
6391
  conversacionId: ctx.conversacionId ?? null,
6279
6392
  status: "error",
6280
- errorMessage: `Required: ${contract.requiredRoles.join(",")}, got: ${ctx.user.rol}`,
6393
+ errorMessage: `Required: ${reqDesc}, got rol: ${ctx.user.rol}`,
6281
6394
  durationMs: Date.now() - startMs
6282
6395
  });
6283
6396
  return {
@@ -6502,9 +6615,24 @@ async function dispatchWithContract(args) {
6502
6615
  const { contract, helper, callable, input, ctx } = args;
6503
6616
  if (getMode() === "admin") {
6504
6617
  const db = getAdminDb();
6618
+ const permissionResolver = async (args2) => {
6619
+ if (args2.userRol === "super_admin") return "completo";
6620
+ const ref = db.collection("configuracion").doc(`${args2.tenantId}_roles`);
6621
+ const snap = await ref.get();
6622
+ if (!snap.exists) return "ninguno";
6623
+ const rolesData = snap.data();
6624
+ const rolData = rolesData[args2.userRol];
6625
+ if (!rolData || rolData.activo === false) return "ninguno";
6626
+ const nivel = rolData.permisosPorModulo?.[args2.modulo];
6627
+ if (nivel === "completo" || nivel === "editar" || nivel === "ver" || nivel === "ninguno") {
6628
+ return nivel;
6629
+ }
6630
+ return "ninguno";
6631
+ };
6505
6632
  const wrapped = wrapWithContract(
6506
6633
  contract,
6507
- async (i) => helper({ ...i, db })
6634
+ async (i) => helper({ ...i, db }),
6635
+ { permissionResolver }
6508
6636
  );
6509
6637
  return wrapped(input, ctx);
6510
6638
  }