/**
* BareMCP — Audit Tools
*
* Tools for viewing and recovering from audit logs:
* - list_audit_logs
* - get_audit_log
* - recover_deleted_resource
*/
import { z } from "zod";
import type { HttpClient } from "../client/index.js";
import {
formatError,
formatSuccess,
StoreIdRequiredError,
storeApiPath,
} from "../client/index.js";
import type { AuditLog, ListResponse, SingleResponse } from "../client/index.js";
import type { ToolDefinition } from "./types.js";
// =============================================================================
// Validation Schemas
// =============================================================================
const auditActorType = z.enum(["merchant", "api_key", "system", "webhook"]);
const auditAction = z.enum(["create", "update", "delete", "export"]);
const auditResourceType = z.enum([
"product",
"page",
"category",
"customer",
"order",
"media",
"webhook",
"api_key",
"store",
"merchant",
"payment_settings",
]);
const listAuditLogsSchema = z.object({
storeId: z.string().uuid().optional(),
actorType: auditActorType.optional(),
action: auditAction.optional(),
resourceType: auditResourceType.optional(),
resourceId: z.string().uuid().optional(),
actorId: z.string().uuid().optional(),
dateFrom: z.string().optional(),
dateTo: z.string().optional(),
sortOrder: z.enum(["asc", "desc"]).default("desc"),
limit: z.number().int().min(1).max(200).default(50),
offset: z.number().int().min(0).default(0),
});
const getAuditLogSchema = z.object({
storeId: z.string().uuid().optional(),
logId: z.string().uuid(),
});
const recoverResourceSchema = z.object({
storeId: z.string().uuid().optional(),
logId: z.string().uuid(),
});
/**
* Resource types that support recovery via audit logs.
*
* NOT recoverable:
* - api_key: Security risk - must create new key
* - media: Hard deleted with storage cleanup
* - store/merchant/payment_settings: Admin-only operations
*/
const RECOVERABLE_RESOURCES = [
"product",
"page",
"category",
"order",
"webhook",
"customer",
] as const;
// =============================================================================
// Helper Functions
// =============================================================================
function resolveStoreId(
providedStoreId: string | undefined,
client: HttpClient
): string {
const storeId = providedStoreId || client.getDefaultStoreId();
if (!storeId) {
throw new StoreIdRequiredError();
}
return storeId;
}
/**
* Format audit log for summary view
*/
function formatAuditLogSummary(log: AuditLog) {
return {
id: log.id,
actorId: log.actorId,
actorType: log.actorType,
action: log.action,
resourceId: log.resourceId,
resourceType: log.resourceType,
ipAddress: log.ipAddress,
createdAt: log.createdAt,
};
}
/**
* Format full audit log for response
*/
function formatAuditLogFull(log: AuditLog) {
return {
id: log.id,
storeId: log.storeId,
actorId: log.actorId,
actorType: log.actorType,
action: log.action,
resourceId: log.resourceId,
resourceType: log.resourceType,
diff: log.diff,
ipAddress: log.ipAddress,
createdAt: log.createdAt,
};
}
// =============================================================================
// Tool Implementations
// =============================================================================
/**
* Create all audit log tools
*/
export function createAuditTools(client: HttpClient): ToolDefinition[] {
return [
// =========================================================================
// list_audit_logs
// =========================================================================
{
name: "list_audit_logs",
description:
"List audit logs showing changes made to store data. Filter by actor, action, resource type, or date range.",
inputSchema: {
type: "object",
properties: {
storeId: {
type: "string",
description: "Store UUID. If not provided, uses the default store.",
},
actorType: {
type: "string",
enum: ["merchant", "api_key", "system", "webhook"],
description: "Filter by type of actor who made the change",
},
action: {
type: "string",
enum: ["create", "update", "delete", "export"],
description: "Filter by action type",
},
resourceType: {
type: "string",
enum: [
"product",
"page",
"category",
"customer",
"order",
"media",
"webhook",
"api_key",
"store",
"merchant",
"payment_settings",
],
description: "Filter by type of resource changed",
},
resourceId: {
type: "string",
description: "Filter by specific resource UUID",
},
actorId: {
type: "string",
description: "Filter by specific actor UUID",
},
dateFrom: {
type: "string",
description: "Show logs after this date (ISO format)",
},
dateTo: {
type: "string",
description: "Show logs before this date (ISO format)",
},
sortOrder: {
type: "string",
enum: ["asc", "desc"],
default: "desc",
description: "Sort direction (newest first by default)",
},
limit: {
type: "integer",
default: 50,
minimum: 1,
maximum: 200,
description: "Number of logs to return",
},
offset: {
type: "integer",
default: 0,
minimum: 0,
description: "Number of logs to skip",
},
},
},
handler: async (args) => {
try {
const input = listAuditLogsSchema.parse(args);
const storeId = resolveStoreId(input.storeId, client);
const params: Record<string, unknown> = {
limit: input.limit,
offset: input.offset,
sortOrder: input.sortOrder,
};
if (input.actorType) params.actorType = input.actorType;
if (input.action) params.action = input.action;
if (input.resourceType) params.resourceType = input.resourceType;
if (input.resourceId) params.resourceId = input.resourceId;
if (input.actorId) params.actorId = input.actorId;
if (input.dateFrom) params.dateFrom = input.dateFrom;
if (input.dateTo) params.dateTo = input.dateTo;
const response = await client.get<ListResponse<AuditLog>>(
storeApiPath(storeId, "audit-logs"),
params
);
return formatSuccess({
items: response.items.map(formatAuditLogSummary),
total: response.pagination.total,
});
} catch (error) {
return formatError(error);
}
},
},
// =========================================================================
// get_audit_log
// =========================================================================
{
name: "get_audit_log",
description:
"Get detailed information about a specific audit log entry including the before/after diff.",
inputSchema: {
type: "object",
properties: {
storeId: {
type: "string",
description: "Store UUID. If not provided, uses the default store.",
},
logId: {
type: "string",
description: "Audit log UUID",
},
},
required: ["logId"],
},
handler: async (args) => {
try {
const input = getAuditLogSchema.parse(args);
const storeId = resolveStoreId(input.storeId, client);
const response = await client.get<SingleResponse<AuditLog>>(
storeApiPath(storeId, `audit-logs/${input.logId}`)
);
return formatSuccess(formatAuditLogFull(response.item));
} catch (error) {
return formatError(error);
}
},
},
// =========================================================================
// recover_deleted_resource
// =========================================================================
{
name: "recover_deleted_resource",
description: `Recover a deleted resource from an audit log snapshot.
REQUIREMENTS:
- Only available on Growth and Enterprise plans
- Only works for "delete" action audit logs with snapshots
- Must be within the plan's retention period (90 days for Growth, unlimited for Enterprise)
RECOVERABLE RESOURCES:
- product: Restored to "draft" status
- page: Restored to "draft" status
- category: Restored to "draft" status
- order: Restored to previous status from snapshot
- webhook: Recreated with a new secret
- customer: Restored from snapshot data
NOT RECOVERABLE:
- api_key: Security risk - create a new key instead
- media: Permanently deleted with storage cleanup`,
inputSchema: {
type: "object",
properties: {
storeId: {
type: "string",
description: "Store UUID. If not provided, uses the default store.",
},
logId: {
type: "string",
description: "The audit log UUID for the delete action to recover from",
},
},
required: ["logId"],
},
handler: async (args) => {
try {
const input = recoverResourceSchema.parse(args);
const storeId = resolveStoreId(input.storeId, client);
// First, get the audit log to validate it's recoverable
const auditResponse = await client.get<SingleResponse<AuditLog>>(
storeApiPath(storeId, `audit-logs/${input.logId}`)
);
const auditLog = auditResponse.item;
// Validate the audit log is for a delete action
if (auditLog.action !== "delete") {
return formatError(
new Error(`Cannot recover from "${auditLog.action}" action. Only "delete" actions can be recovered.`)
);
}
// Validate the resource type is recoverable
const resourceType = auditLog.resourceType as string;
if (!RECOVERABLE_RESOURCES.includes(resourceType as typeof RECOVERABLE_RESOURCES[number])) {
const notRecoverableReasons: Record<string, string> = {
api_key: "API keys cannot be recovered for security reasons. Please create a new API key instead.",
media: "Media files are permanently deleted along with their storage. Please upload the file again.",
store: "Store recovery is not supported via this tool.",
merchant: "Merchant recovery is not supported via this tool.",
payment_settings: "Payment settings recovery is not supported via this tool.",
};
const reason = notRecoverableReasons[resourceType] || `Resource type "${resourceType}" is not recoverable.`;
return formatError(new Error(reason));
}
// Call the recovery endpoint
const response = await client.post<{ message: string; resourceId: string; resourceType: string }>(
storeApiPath(storeId, `audit-logs/${input.logId}/recover`),
{}
);
return formatSuccess({
recovered: true,
message: response.message,
resourceId: response.resourceId,
resourceType: response.resourceType,
});
} catch (error) {
return formatError(error);
}
},
},
];
}