ponch-mcp-server 1.0.73 → 1.0.75
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 +205 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
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
|
-
|
|
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}'
|
|
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: ${
|
|
6393
|
+
errorMessage: `Required: ${reqDesc}, got rol: ${ctx.user.rol}`,
|
|
6281
6394
|
durationMs: Date.now() - startMs
|
|
6282
6395
|
});
|
|
6283
6396
|
return {
|
|
@@ -6288,6 +6401,19 @@ function wrapWithContract(contract, helper) {
|
|
|
6288
6401
|
}
|
|
6289
6402
|
const parseResult = contract.paramsSchema.safeParse(input);
|
|
6290
6403
|
if (!parseResult.success) {
|
|
6404
|
+
const validationErrors = parseResult.error.issues.map((i) => {
|
|
6405
|
+
const issue = i;
|
|
6406
|
+
return {
|
|
6407
|
+
path: i.path,
|
|
6408
|
+
code: i.code,
|
|
6409
|
+
message: i.message,
|
|
6410
|
+
received: issue.received,
|
|
6411
|
+
expected: issue.expected
|
|
6412
|
+
};
|
|
6413
|
+
});
|
|
6414
|
+
const camposFallidos = validationErrors.map((v) => v.path.join(".")).filter(Boolean);
|
|
6415
|
+
const baseMsg = getWrapperMessage("input_invalido", locale);
|
|
6416
|
+
const text = camposFallidos.length ? locale === "en" ? `${baseMsg} Issues with: ${camposFallidos.join(", ")}.` : `${baseMsg} Hubo problemas con: ${camposFallidos.join(", ")}.` : baseMsg;
|
|
6291
6417
|
await writeAuditLog({
|
|
6292
6418
|
tenantId: ctx.tenantId,
|
|
6293
6419
|
brandId: ctx.brandId ?? null,
|
|
@@ -6296,13 +6422,14 @@ function wrapWithContract(contract, helper) {
|
|
|
6296
6422
|
motivo: "Input inv\xE1lido \u2014 Zod parse failed",
|
|
6297
6423
|
conversacionId: ctx.conversacionId ?? null,
|
|
6298
6424
|
status: "error",
|
|
6299
|
-
errorMessage: `Input shape inv\xE1lido: ${
|
|
6425
|
+
errorMessage: `Input shape inv\xE1lido: ${validationErrors.map((v) => `${v.path.join(".")}: ${v.code} (${v.message})`).join("; ")}`,
|
|
6300
6426
|
durationMs: Date.now() - startMs
|
|
6301
6427
|
});
|
|
6302
6428
|
return {
|
|
6303
|
-
text
|
|
6429
|
+
text,
|
|
6304
6430
|
structuredOutput: null,
|
|
6305
|
-
state: "error"
|
|
6431
|
+
state: "error",
|
|
6432
|
+
validationErrors
|
|
6306
6433
|
};
|
|
6307
6434
|
}
|
|
6308
6435
|
const parsedInput = parseResult.data;
|
|
@@ -6349,7 +6476,6 @@ function wrapWithContract(contract, helper) {
|
|
|
6349
6476
|
let output;
|
|
6350
6477
|
try {
|
|
6351
6478
|
output = await helper(parsedInput);
|
|
6352
|
-
contract.outputSchema.parse(output);
|
|
6353
6479
|
} catch (err) {
|
|
6354
6480
|
await writeAuditLog({
|
|
6355
6481
|
tenantId: ctx.tenantId,
|
|
@@ -6368,6 +6494,36 @@ function wrapWithContract(contract, helper) {
|
|
|
6368
6494
|
state: "error"
|
|
6369
6495
|
};
|
|
6370
6496
|
}
|
|
6497
|
+
const outputParse = contract.outputSchema.safeParse(output);
|
|
6498
|
+
if (!outputParse.success) {
|
|
6499
|
+
const validationErrors = outputParse.error.issues.map((i) => {
|
|
6500
|
+
const issue = i;
|
|
6501
|
+
return {
|
|
6502
|
+
path: i.path,
|
|
6503
|
+
code: i.code,
|
|
6504
|
+
message: i.message,
|
|
6505
|
+
received: issue.received,
|
|
6506
|
+
expected: issue.expected
|
|
6507
|
+
};
|
|
6508
|
+
});
|
|
6509
|
+
await writeAuditLog({
|
|
6510
|
+
tenantId: ctx.tenantId,
|
|
6511
|
+
brandId: ctx.brandId ?? null,
|
|
6512
|
+
actor: { type: "martin", uid: ctx.user.uid, nombre: ctx.user.nombre },
|
|
6513
|
+
action: contract.auditAction,
|
|
6514
|
+
motivo: `Output del helper inv\xE1lido \u2014 bug de "${contract.name}"`,
|
|
6515
|
+
conversacionId: ctx.conversacionId ?? null,
|
|
6516
|
+
status: "error",
|
|
6517
|
+
errorMessage: `Output shape inv\xE1lido: ${validationErrors.map((v) => `${v.path.join(".")}: ${v.code} (${v.message})`).join("; ")}`,
|
|
6518
|
+
durationMs: Date.now() - startMs
|
|
6519
|
+
});
|
|
6520
|
+
return {
|
|
6521
|
+
text: martinSafeError(new Error("output_invalid"), locale),
|
|
6522
|
+
structuredOutput: null,
|
|
6523
|
+
state: "error",
|
|
6524
|
+
validationErrors
|
|
6525
|
+
};
|
|
6526
|
+
}
|
|
6371
6527
|
const maybeDisabled = output;
|
|
6372
6528
|
if (maybeDisabled.disabled === true) {
|
|
6373
6529
|
const code = maybeDisabled.code;
|
|
@@ -6502,9 +6658,24 @@ async function dispatchWithContract(args) {
|
|
|
6502
6658
|
const { contract, helper, callable, input, ctx } = args;
|
|
6503
6659
|
if (getMode() === "admin") {
|
|
6504
6660
|
const db = getAdminDb();
|
|
6661
|
+
const permissionResolver = async (args2) => {
|
|
6662
|
+
if (args2.userRol === "super_admin") return "completo";
|
|
6663
|
+
const ref = db.collection("configuracion").doc(`${args2.tenantId}_roles`);
|
|
6664
|
+
const snap = await ref.get();
|
|
6665
|
+
if (!snap.exists) return "ninguno";
|
|
6666
|
+
const rolesData = snap.data();
|
|
6667
|
+
const rolData = rolesData[args2.userRol];
|
|
6668
|
+
if (!rolData || rolData.activo === false) return "ninguno";
|
|
6669
|
+
const nivel = rolData.permisosPorModulo?.[args2.modulo];
|
|
6670
|
+
if (nivel === "completo" || nivel === "editar" || nivel === "ver" || nivel === "ninguno") {
|
|
6671
|
+
return nivel;
|
|
6672
|
+
}
|
|
6673
|
+
return "ninguno";
|
|
6674
|
+
};
|
|
6505
6675
|
const wrapped = wrapWithContract(
|
|
6506
6676
|
contract,
|
|
6507
|
-
async (i) => helper({ ...i, db })
|
|
6677
|
+
async (i) => helper({ ...i, db }),
|
|
6678
|
+
{ permissionResolver }
|
|
6508
6679
|
);
|
|
6509
6680
|
return wrapped(input, ctx);
|
|
6510
6681
|
}
|
|
@@ -7710,7 +7881,14 @@ function registerMarketingTools(server, session) {
|
|
|
7710
7881
|
input: { tenantId, brandId, plan },
|
|
7711
7882
|
ctx
|
|
7712
7883
|
});
|
|
7713
|
-
const payload = result.state === "success" ? result.structuredOutput : {
|
|
7884
|
+
const payload = result.state === "success" ? result.structuredOutput : {
|
|
7885
|
+
ok: false,
|
|
7886
|
+
state: result.state,
|
|
7887
|
+
mensaje: result.text,
|
|
7888
|
+
// HITO 6 A6.7: incluir detalles Zod estructurados para que
|
|
7889
|
+
// Claude (LLM) pueda auto-recuperarse en el siguiente intento.
|
|
7890
|
+
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
7891
|
+
};
|
|
7714
7892
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7715
7893
|
}
|
|
7716
7894
|
);
|
|
@@ -7918,7 +8096,14 @@ Si pasas campos dentro de "datos", se hace merge con los datos existentes (no lo
|
|
|
7918
8096
|
input: { tenantId, brandId, mes, semana, slotIndex, cambios, accionContenidoExistente },
|
|
7919
8097
|
ctx
|
|
7920
8098
|
});
|
|
7921
|
-
const payload = result.state === "success" ? result.structuredOutput : {
|
|
8099
|
+
const payload = result.state === "success" ? result.structuredOutput : {
|
|
8100
|
+
ok: false,
|
|
8101
|
+
state: result.state,
|
|
8102
|
+
mensaje: result.text,
|
|
8103
|
+
// HITO 6 A6.7: incluir detalles Zod estructurados para que
|
|
8104
|
+
// Claude (LLM) pueda auto-recuperarse en el siguiente intento.
|
|
8105
|
+
...result.validationErrors && result.validationErrors.length > 0 ? { validationErrors: result.validationErrors } : {}
|
|
8106
|
+
};
|
|
7922
8107
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7923
8108
|
}
|
|
7924
8109
|
);
|