funifier-mcp 0.3.9 → 0.3.11
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/.cursor/rules/funifier.mdc +4 -1
- package/.github/copilot-instructions.md +3 -0
- package/AGENTS.md +6 -44
- package/README.md +13 -1
- package/datasource-funifier-docs/.coverage.json +4 -1
- package/dist/cli/config-writers.d.ts +3 -3
- package/dist/cli/config-writers.d.ts.map +1 -1
- package/dist/cli/config-writers.js +22 -24
- package/dist/cli/config-writers.js.map +1 -1
- package/dist/cli/config-writers.test.js +7 -2
- package/dist/cli/config-writers.test.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +11 -9
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +26 -0
- package/dist/cli/init.test.js.map +1 -1
- package/dist/core/api-client.d.ts +12 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +95 -0
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +6 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +7 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Role.d.ts +25 -0
- package/dist/core/types/Role.d.ts.map +1 -0
- package/dist/core/types/Role.js +3 -0
- package/dist/core/types/Role.js.map +1 -0
- package/dist/core/types/index.d.ts +1 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +1 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +103 -98
- package/dist/mcp/tools/_backup.d.ts +74 -0
- package/dist/mcp/tools/_backup.d.ts.map +1 -0
- package/dist/mcp/tools/_backup.js +205 -0
- package/dist/mcp/tools/_backup.js.map +1 -0
- package/dist/mcp/tools/_backup.test.d.ts +2 -0
- package/dist/mcp/tools/_backup.test.d.ts.map +1 -0
- package/dist/mcp/tools/_backup.test.js +204 -0
- package/dist/mcp/tools/_backup.test.js.map +1 -0
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +4 -0
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list-tools.d.ts.map +1 -1
- package/dist/mcp/tools/list-tools.js +7 -0
- package/dist/mcp/tools/list-tools.js.map +1 -1
- package/dist/mcp/tools/list-tools.test.js +2 -1
- package/dist/mcp/tools/list-tools.test.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +13 -0
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/permissions.d.ts +4 -0
- package/dist/mcp/tools/permissions.d.ts.map +1 -0
- package/dist/mcp/tools/permissions.js +377 -0
- package/dist/mcp/tools/permissions.js.map +1 -0
- package/dist/mcp/tools/permissions.test.d.ts +2 -0
- package/dist/mcp/tools/permissions.test.d.ts.map +1 -0
- package/dist/mcp/tools/permissions.test.js +355 -0
- package/dist/mcp/tools/permissions.test.js.map +1 -0
- package/package.json +2 -2
- package/skills/funifier/SKILL.md +1 -0
- package/skills/funifier/references/configure-security.md +18 -0
- package/skills/funifier/references/create-aggregate.md +6 -0
- package/skills/funifier/references/date-handling.md +172 -0
- package/skills/funifier/references/debug.md +2 -0
- package/skills/funifier/references/implement-frontend.md +18 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const permissions_1 = require("./permissions");
|
|
11
|
+
const _backup_1 = require("./_backup");
|
|
12
|
+
// Every mutating action now writes a snapshot to backupRoot(); redirect it to a temp dir
|
|
13
|
+
// (via the FUNIFIER_BACKUP_ROOT seam) so tests never touch the real working tree.
|
|
14
|
+
let backupDir;
|
|
15
|
+
(0, vitest_1.beforeEach)(() => {
|
|
16
|
+
backupDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "funifier-perm-test-"));
|
|
17
|
+
process.env.FUNIFIER_BACKUP_ROOT = backupDir;
|
|
18
|
+
});
|
|
19
|
+
(0, vitest_1.afterEach)(() => {
|
|
20
|
+
delete process.env.FUNIFIER_BACKUP_ROOT;
|
|
21
|
+
fs_1.default.rmSync(backupDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
function readNewestSnapshot() {
|
|
24
|
+
const items = (0, _backup_1.listSnapshots)(backupDir);
|
|
25
|
+
if (items.length === 0)
|
|
26
|
+
throw new Error("no snapshot written");
|
|
27
|
+
return JSON.parse(fs_1.default.readFileSync(items[0].path, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
const SECURITY = {
|
|
30
|
+
_id: "game1",
|
|
31
|
+
apps: [{ name: "Default", app_secret: "old-secret", scope: "read_all" }],
|
|
32
|
+
roles: [{ name: "player", timeout: "1d", scope: "read_all,write_actionlog" }],
|
|
33
|
+
};
|
|
34
|
+
function fakeApi(overrides = {}) {
|
|
35
|
+
const roles = [
|
|
36
|
+
{ _id: "r1", name: "Admin", type: "system", item: "system", permissions: [] },
|
|
37
|
+
{ _id: "r2", name: "Game Editor", type: "gamification", item: "game1", permissions: [] },
|
|
38
|
+
];
|
|
39
|
+
const assignments = [{ type: "user", item: "u1", role: "r1" }];
|
|
40
|
+
return {
|
|
41
|
+
queryCollection: vitest_1.vi.fn().mockResolvedValue([structuredClone(SECURITY)]),
|
|
42
|
+
updateDocument: vitest_1.vi.fn().mockImplementation((_collection, doc) => Promise.resolve(doc)),
|
|
43
|
+
listRoles: vitest_1.vi.fn().mockResolvedValue(roles),
|
|
44
|
+
listRolesStrict: vitest_1.vi.fn().mockResolvedValue(roles),
|
|
45
|
+
getRoleById: vitest_1.vi.fn().mockResolvedValue({ _id: "r1", name: "Admin", type: "system", item: "system", permissions: [] }),
|
|
46
|
+
saveRole: vitest_1.vi.fn().mockImplementation((role) => Promise.resolve({ ...role, _id: role._id ?? "new-role" })),
|
|
47
|
+
deleteRole: vitest_1.vi.fn().mockResolvedValue({ ok: 1 }),
|
|
48
|
+
listRoleAssignments: vitest_1.vi.fn().mockResolvedValue(assignments),
|
|
49
|
+
listRoleAssignmentsStrict: vitest_1.vi.fn().mockResolvedValue(assignments),
|
|
50
|
+
assignRole: vitest_1.vi.fn().mockImplementation((payload) => Promise.resolve(payload)),
|
|
51
|
+
unassignRole: vitest_1.vi.fn().mockResolvedValue({ ok: 1 }),
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function makeInvoke(overrides = {}, connection = { serverUrl: "https://test.funifier.com" }) {
|
|
56
|
+
const api = fakeApi(overrides);
|
|
57
|
+
const holder = {
|
|
58
|
+
requireClient: () => api,
|
|
59
|
+
getConnectionInfo: () => ({
|
|
60
|
+
connected: true,
|
|
61
|
+
serverUrl: connection.serverUrl ?? null,
|
|
62
|
+
name: null,
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
let handler;
|
|
66
|
+
const server = {
|
|
67
|
+
registerTool: (_name, _config, h) => { handler = h; },
|
|
68
|
+
};
|
|
69
|
+
(0, permissions_1.registerPermissionsTool)(server, holder);
|
|
70
|
+
return { invoke: (args) => handler(args), api };
|
|
71
|
+
}
|
|
72
|
+
(0, vitest_1.describe)("funifier_permissions", () => {
|
|
73
|
+
(0, vitest_1.it)("lists API applications from the security document", async () => {
|
|
74
|
+
const { invoke, api } = makeInvoke();
|
|
75
|
+
const result = await invoke({ action: "list_api_apps" });
|
|
76
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
77
|
+
(0, vitest_1.expect)(api.queryCollection).toHaveBeenCalledWith("security", {}, { limit: 1, skip: 0 });
|
|
78
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("old-…cret");
|
|
79
|
+
(0, vitest_1.expect)(result.content[0].text).not.toContain("old-secret");
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)("upserts an API app by app_secret", async () => {
|
|
82
|
+
const { invoke, api } = makeInvoke();
|
|
83
|
+
const result = await invoke({
|
|
84
|
+
action: "save_api_app",
|
|
85
|
+
data: JSON.stringify({ name: "Default Updated", app_secret: "old-secret", scope: "read_all,write_all" }),
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
88
|
+
(0, vitest_1.expect)(api.updateDocument).toHaveBeenCalledWith("security", vitest_1.expect.objectContaining({
|
|
89
|
+
apps: [{ name: "Default Updated", app_secret: "old-secret", scope: "read_all,write_all" }],
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("removes a security role by name", async () => {
|
|
93
|
+
const { invoke, api } = makeInvoke();
|
|
94
|
+
const result = await invoke({ action: "delete_security_role", name: "player" });
|
|
95
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
96
|
+
(0, vitest_1.expect)(api.updateDocument).toHaveBeenCalledWith("security", vitest_1.expect.objectContaining({ roles: [] }));
|
|
97
|
+
});
|
|
98
|
+
(0, vitest_1.it)("filters Studio roles by type and item", async () => {
|
|
99
|
+
const { invoke } = makeInvoke();
|
|
100
|
+
const result = await invoke({ action: "list_studio_roles", role_type: "gamification", item: "game1" });
|
|
101
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
102
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Game Editor");
|
|
103
|
+
(0, vitest_1.expect)(result.content[0].text).not.toContain("Admin");
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)("saves a Studio role with permissions", async () => {
|
|
106
|
+
const { invoke, api } = makeInvoke();
|
|
107
|
+
const role = {
|
|
108
|
+
name: "Editor",
|
|
109
|
+
type: "gamification",
|
|
110
|
+
item: "game1",
|
|
111
|
+
permissions: [{ type: "page", object: "/studio/security", operations: { read: true, update: true } }],
|
|
112
|
+
};
|
|
113
|
+
const result = await invoke({ action: "save_studio_role", data: JSON.stringify(role) });
|
|
114
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
115
|
+
(0, vitest_1.expect)(api.saveRole).toHaveBeenCalledWith(role);
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)("assigns a Studio role using individual fields", async () => {
|
|
118
|
+
const { invoke, api } = makeInvoke();
|
|
119
|
+
const result = await invoke({
|
|
120
|
+
action: "assign_studio_role",
|
|
121
|
+
principal_type: "user",
|
|
122
|
+
principal_item: "u1",
|
|
123
|
+
role_id: "r1",
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
126
|
+
(0, vitest_1.expect)(api.assignRole).toHaveBeenCalledWith({ type: "user", item: "u1", role: "r1" });
|
|
127
|
+
});
|
|
128
|
+
(0, vitest_1.it)("returns isError for invalid JSON payloads", async () => {
|
|
129
|
+
const { invoke } = makeInvoke();
|
|
130
|
+
const result = await invoke({ action: "save_api_app", data: "not-json" });
|
|
131
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
132
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Error in funifier_permissions");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.describe)("funifier_permissions native pre-write backup (no LLM in the loop)", () => {
|
|
136
|
+
(0, vitest_1.it)("writes a snapshot of the prior security doc BEFORE the mutation fires", async () => {
|
|
137
|
+
// The mutation mock records how many snapshots exist at the moment it is invoked.
|
|
138
|
+
let snapshotsWhenMutated = -1;
|
|
139
|
+
const { invoke, api } = makeInvoke({
|
|
140
|
+
updateDocument: vitest_1.vi.fn().mockImplementation((_collection, doc) => {
|
|
141
|
+
snapshotsWhenMutated = (0, _backup_1.listSnapshots)(backupDir).length;
|
|
142
|
+
return Promise.resolve(doc);
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
// A genuinely new app, so the mutated doc (2 apps) differs from the prior doc (1 app).
|
|
146
|
+
const result = await invoke({
|
|
147
|
+
action: "save_api_app",
|
|
148
|
+
data: JSON.stringify({ name: "New App", app_secret: "brand-new", scope: "read_all" }),
|
|
149
|
+
});
|
|
150
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
151
|
+
(0, vitest_1.expect)(api.updateDocument).toHaveBeenCalled();
|
|
152
|
+
(0, vitest_1.expect)(snapshotsWhenMutated).toBe(1); // backup existed before updateDocument ran
|
|
153
|
+
const snap = readNewestSnapshot();
|
|
154
|
+
(0, vitest_1.expect)(snap.category).toBe("security");
|
|
155
|
+
(0, vitest_1.expect)(snap.existedBefore).toBe(true);
|
|
156
|
+
(0, vitest_1.expect)(snap.preImage).toEqual(SECURITY); // whole PRIOR doc, not the mutated 2-app version
|
|
157
|
+
});
|
|
158
|
+
(0, vitest_1.it)("aborts the mutation and returns isError when the backup write fails", async () => {
|
|
159
|
+
// Point the backup root at a path blocked by a file → mkdir for the category dir throws.
|
|
160
|
+
const blocker = path_1.default.join(backupDir, "blocker");
|
|
161
|
+
fs_1.default.writeFileSync(blocker, "not-a-directory");
|
|
162
|
+
process.env.FUNIFIER_BACKUP_ROOT = path_1.default.join(blocker, "nested");
|
|
163
|
+
const { invoke, api } = makeInvoke();
|
|
164
|
+
const result = await invoke({
|
|
165
|
+
action: "save_api_app",
|
|
166
|
+
data: JSON.stringify({ name: "Default", app_secret: "old-secret", scope: "read_all" }),
|
|
167
|
+
});
|
|
168
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
169
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Backup write failed (mutation aborted)");
|
|
170
|
+
(0, vitest_1.expect)(api.updateDocument).not.toHaveBeenCalled(); // the literal "native, not LLM" guarantee
|
|
171
|
+
});
|
|
172
|
+
(0, vitest_1.it)("records existedBefore=false / preImage=null when creating a brand-new studio role", async () => {
|
|
173
|
+
const { invoke, api } = makeInvoke();
|
|
174
|
+
const role = {
|
|
175
|
+
name: "Brand New",
|
|
176
|
+
type: "gamification",
|
|
177
|
+
item: "game1",
|
|
178
|
+
permissions: [{ type: "page", object: "/x", operations: { read: true } }],
|
|
179
|
+
};
|
|
180
|
+
const result = await invoke({ action: "save_studio_role", data: JSON.stringify(role) });
|
|
181
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
182
|
+
(0, vitest_1.expect)(api.saveRole).toHaveBeenCalledWith(role);
|
|
183
|
+
const snap = readNewestSnapshot(); // built by a real captureBackup, not a hand-faked fixture
|
|
184
|
+
(0, vitest_1.expect)(snap.category).toBe("studio-role");
|
|
185
|
+
(0, vitest_1.expect)(snap.existedBefore).toBe(false);
|
|
186
|
+
(0, vitest_1.expect)(snap.preImage).toBeNull();
|
|
187
|
+
(0, vitest_1.expect)(snap.naturalKey).toEqual({ name: "Brand New", type: "gamification", item: "game1" });
|
|
188
|
+
});
|
|
189
|
+
vitest_1.it.each([
|
|
190
|
+
["save_api_app", { data: JSON.stringify({ name: "X", app_secret: "s", scope: "read_all" }) }],
|
|
191
|
+
["delete_api_app", { app_secret: "old-secret" }],
|
|
192
|
+
["save_security_role", { data: JSON.stringify({ name: "X", timeout: "1d", scope: "read_all" }) }],
|
|
193
|
+
["delete_security_role", { name: "player" }],
|
|
194
|
+
["save_studio_role", { data: JSON.stringify({ name: "X", type: "system", item: "system", permissions: [] }) }],
|
|
195
|
+
["delete_studio_role", { role_id: "r1" }],
|
|
196
|
+
["assign_studio_role", { principal_type: "user", principal_item: "u1", role_id: "r1" }],
|
|
197
|
+
["unassign_studio_role", { principal_type: "user", principal_item: "u1", role_id: "r1" }],
|
|
198
|
+
])("%s writes exactly one pre-write snapshot", async (action, args) => {
|
|
199
|
+
const { invoke } = makeInvoke();
|
|
200
|
+
const result = await invoke({ action, ...args });
|
|
201
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
202
|
+
(0, vitest_1.expect)((0, _backup_1.listSnapshots)(backupDir).length).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
(0, vitest_1.it)("restore_backup reapplies a security snapshot via updateDocument", async () => {
|
|
205
|
+
// 1. A real mutation captures the prior security doc.
|
|
206
|
+
const writer = makeInvoke();
|
|
207
|
+
await writer.invoke({
|
|
208
|
+
action: "save_api_app",
|
|
209
|
+
data: JSON.stringify({ name: "Default", app_secret: "old-secret", scope: "read_all" }),
|
|
210
|
+
});
|
|
211
|
+
(0, vitest_1.expect)((0, _backup_1.listSnapshots)(backupDir).length).toBe(1);
|
|
212
|
+
const backupPath = (0, _backup_1.listSnapshots)(backupDir)[0].path;
|
|
213
|
+
// 2. A fresh handler restores it — restore writes its own pre-write backup (BKP-11).
|
|
214
|
+
const { invoke, api } = makeInvoke();
|
|
215
|
+
const result = await invoke({ action: "restore_backup", backup_path: backupPath });
|
|
216
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
217
|
+
(0, vitest_1.expect)(api.updateDocument).toHaveBeenCalledWith("security", vitest_1.expect.objectContaining({ _id: "game1" }));
|
|
218
|
+
(0, vitest_1.expect)((0, _backup_1.listSnapshots)(backupDir).length).toBe(2);
|
|
219
|
+
});
|
|
220
|
+
(0, vitest_1.it)("restore_backup rejects cross-server snapshots when serverUrl differs", async () => {
|
|
221
|
+
const writer = makeInvoke({}, { serverUrl: "https://tenant-a.funifier.com" });
|
|
222
|
+
await writer.invoke({
|
|
223
|
+
action: "save_api_app",
|
|
224
|
+
data: JSON.stringify({ name: "Default", app_secret: "old-secret", scope: "read_all" }),
|
|
225
|
+
});
|
|
226
|
+
const backupPath = (0, _backup_1.listSnapshots)(backupDir)[0].path;
|
|
227
|
+
const { invoke, api } = makeInvoke({}, { serverUrl: "https://tenant-b.funifier.com" });
|
|
228
|
+
const result = await invoke({ action: "restore_backup", backup_path: backupPath });
|
|
229
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
230
|
+
(0, vitest_1.expect)(result.content[0].text).toMatch(/serverUrl mismatch/i);
|
|
231
|
+
(0, vitest_1.expect)(api.updateDocument).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
(0, vitest_1.it)("delete_studio_role aborts when getRoleById fails to read the role", async () => {
|
|
234
|
+
const { invoke, api } = makeInvoke({
|
|
235
|
+
getRoleById: vitest_1.vi.fn().mockResolvedValue(undefined),
|
|
236
|
+
});
|
|
237
|
+
const result = await invoke({ action: "delete_studio_role", role_id: "r1" });
|
|
238
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
239
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("not found or unreadable");
|
|
240
|
+
(0, vitest_1.expect)(api.deleteRole).not.toHaveBeenCalled();
|
|
241
|
+
(0, vitest_1.expect)((0, _backup_1.listSnapshots)(backupDir).length).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
(0, vitest_1.it)("save_studio_role aborts when listRolesStrict fails (avoids false absent snapshot)", async () => {
|
|
244
|
+
const { invoke, api } = makeInvoke({
|
|
245
|
+
listRolesStrict: vitest_1.vi.fn().mockRejectedValue(new Error("network down")),
|
|
246
|
+
});
|
|
247
|
+
const result = await invoke({
|
|
248
|
+
action: "save_studio_role",
|
|
249
|
+
data: JSON.stringify({
|
|
250
|
+
name: "Editor",
|
|
251
|
+
type: "gamification",
|
|
252
|
+
item: "game1",
|
|
253
|
+
permissions: [{ type: "page", object: "/x", operations: { read: true } }],
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
257
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Failed to capture pre-image");
|
|
258
|
+
(0, vitest_1.expect)(api.saveRole).not.toHaveBeenCalled();
|
|
259
|
+
(0, vitest_1.expect)((0, _backup_1.listSnapshots)(backupDir).length).toBe(0);
|
|
260
|
+
});
|
|
261
|
+
(0, vitest_1.it)("assign_studio_role aborts when listRoleAssignmentsStrict fails (avoids false absent snapshot)", async () => {
|
|
262
|
+
const { invoke, api } = makeInvoke({
|
|
263
|
+
listRoleAssignmentsStrict: vitest_1.vi.fn().mockRejectedValue(new Error("network down")),
|
|
264
|
+
});
|
|
265
|
+
const result = await invoke({
|
|
266
|
+
action: "assign_studio_role",
|
|
267
|
+
principal_type: "user",
|
|
268
|
+
principal_item: "u1",
|
|
269
|
+
role_id: "r1",
|
|
270
|
+
});
|
|
271
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
272
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Failed to capture pre-image");
|
|
273
|
+
(0, vitest_1.expect)(api.assignRole).not.toHaveBeenCalled();
|
|
274
|
+
(0, vitest_1.expect)((0, _backup_1.listSnapshots)(backupDir).length).toBe(0);
|
|
275
|
+
});
|
|
276
|
+
(0, vitest_1.it)("restore_backup of an absent studio role resolves the _id via natural key and deletes it", async () => {
|
|
277
|
+
// 1. Create-path snapshot from a real save of a role absent in listRoles → existedBefore=false.
|
|
278
|
+
const writer = makeInvoke();
|
|
279
|
+
await writer.invoke({
|
|
280
|
+
action: "save_studio_role",
|
|
281
|
+
data: JSON.stringify({
|
|
282
|
+
name: "Brand New",
|
|
283
|
+
type: "gamification",
|
|
284
|
+
item: "game1",
|
|
285
|
+
permissions: [{ type: "page", object: "/x", operations: { read: true } }],
|
|
286
|
+
}),
|
|
287
|
+
});
|
|
288
|
+
const backupPath = (0, _backup_1.listSnapshots)(backupDir)[0].path;
|
|
289
|
+
// 2. At restore time the role now exists with a server-assigned _id.
|
|
290
|
+
const { invoke, api } = makeInvoke({
|
|
291
|
+
listRoles: vitest_1.vi.fn().mockResolvedValue([
|
|
292
|
+
{ _id: "created-id", name: "Brand New", type: "gamification", item: "game1", permissions: [] },
|
|
293
|
+
]),
|
|
294
|
+
});
|
|
295
|
+
const result = await invoke({ action: "restore_backup", backup_path: backupPath });
|
|
296
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
297
|
+
(0, vitest_1.expect)(api.deleteRole).toHaveBeenCalledWith("created-id");
|
|
298
|
+
});
|
|
299
|
+
(0, vitest_1.it)("restore_backup with a missing path errors and performs no remote write", async () => {
|
|
300
|
+
const { invoke, api } = makeInvoke();
|
|
301
|
+
const result = await invoke({ action: "restore_backup", backup_path: path_1.default.join(backupDir, "missing.json") });
|
|
302
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
303
|
+
(0, vitest_1.expect)(api.updateDocument).not.toHaveBeenCalled();
|
|
304
|
+
(0, vitest_1.expect)(api.deleteRole).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
(0, vitest_1.it)("restore_backup rejects path traversal outside the backup root", async () => {
|
|
307
|
+
const outside = path_1.default.join(backupDir, "..", "outside-backup.json");
|
|
308
|
+
fs_1.default.writeFileSync(outside, JSON.stringify({
|
|
309
|
+
version: 1,
|
|
310
|
+
action: "save_api_app",
|
|
311
|
+
category: "security",
|
|
312
|
+
resourceId: "game1",
|
|
313
|
+
existedBefore: true,
|
|
314
|
+
capturedAt: "2026-05-28T14:31:02.123Z",
|
|
315
|
+
serverUrl: null,
|
|
316
|
+
preImage: SECURITY,
|
|
317
|
+
}));
|
|
318
|
+
const { invoke, api } = makeInvoke();
|
|
319
|
+
const result = await invoke({ action: "restore_backup", backup_path: outside });
|
|
320
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
321
|
+
(0, vitest_1.expect)(result.content[0].text).toMatch(/must be within/i);
|
|
322
|
+
(0, vitest_1.expect)(api.updateDocument).not.toHaveBeenCalled();
|
|
323
|
+
fs_1.default.rmSync(outside, { force: true });
|
|
324
|
+
});
|
|
325
|
+
(0, vitest_1.it)("restore_backup errors when a studio-role snapshot lacks naturalKey", async () => {
|
|
326
|
+
const badPath = path_1.default.join(backupDir, "studio-role", "missing-key.json");
|
|
327
|
+
fs_1.default.mkdirSync(path_1.default.dirname(badPath), { recursive: true });
|
|
328
|
+
fs_1.default.writeFileSync(badPath, JSON.stringify({
|
|
329
|
+
version: 1,
|
|
330
|
+
action: "save_studio_role",
|
|
331
|
+
category: "studio-role",
|
|
332
|
+
resourceId: "r1",
|
|
333
|
+
existedBefore: false,
|
|
334
|
+
capturedAt: "2026-05-28T14:31:02.123Z",
|
|
335
|
+
serverUrl: null,
|
|
336
|
+
preImage: null,
|
|
337
|
+
}));
|
|
338
|
+
const { invoke, api } = makeInvoke();
|
|
339
|
+
const result = await invoke({ action: "restore_backup", backup_path: badPath });
|
|
340
|
+
(0, vitest_1.expect)(result.isError).toBe(true);
|
|
341
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Studio-role snapshot missing naturalKey");
|
|
342
|
+
(0, vitest_1.expect)(api.saveRole).not.toHaveBeenCalled();
|
|
343
|
+
(0, vitest_1.expect)(api.deleteRole).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
(0, vitest_1.it)("list_backups returns captured snapshots newest-first", async () => {
|
|
346
|
+
const writer = makeInvoke();
|
|
347
|
+
await writer.invoke({ action: "delete_security_role", name: "player" });
|
|
348
|
+
const { invoke } = makeInvoke();
|
|
349
|
+
const result = await invoke({ action: "list_backups" });
|
|
350
|
+
(0, vitest_1.expect)(result.isError).toBeUndefined();
|
|
351
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("Backups (1)");
|
|
352
|
+
(0, vitest_1.expect)(result.content[0].text).toContain("security");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
//# sourceMappingURL=permissions.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.test.js","sourceRoot":"","sources":["../../../src/mcp/tools/permissions.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "funifier-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Funifier AI toolkit — MCP server, API client, and Claude Code skills for the Funifier gamification platform",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -72,4 +72,4 @@
|
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=18"
|
|
74
74
|
}
|
|
75
|
-
}
|
|
75
|
+
}
|
package/skills/funifier/SKILL.md
CHANGED
|
@@ -36,6 +36,7 @@ Then read a result with `funifier_read_doc path=<path>`.
|
|
|
36
36
|
| Create a Funifier trigger — guided workflow from entity/event selection to Groovy code generation | `references/create-trigger.md` |
|
|
37
37
|
| Create a Funifier scheduler — guided workflow from naming and cron expression to Groovy code generation | `references/create-scheduler.md` |
|
|
38
38
|
| Create a Funifier prepared aggregate — guided workflow for building MongoDB aggregate pipelines for reports and dashboards | `references/create-aggregate.md` |
|
|
39
|
+
| Create, edit, or query Date fields safely via REST, funifier_database, triggers, schedulers, aggregate, and prepared aggregate; use whenever a payload or filter contains dates | `references/date-handling.md` |
|
|
39
40
|
| Create a Funifier challenge — missions with action rules and point rewards | `references/create-challenge.md` |
|
|
40
41
|
| Create a Funifier action — define trackable player behaviors with attributes | `references/create-action.md` |
|
|
41
42
|
| Create a Funifier point category — XP, coins, or custom metrics | `references/create-point.md` |
|
|
@@ -94,3 +94,21 @@ GET /v3/database/security/<API_KEY>?strict=true
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Teste login de jogador e confirme que o token retornado tem os scopes corretos.
|
|
97
|
+
|
|
98
|
+
### 7. Backup automático e rollback (funifier_permissions)
|
|
99
|
+
|
|
100
|
+
Toda mutação via `funifier_permissions` grava um snapshot completo em `.funifier/backups/` **antes** da escrita remota. Snapshots podem conter `app_secret` e escopos — mantenha `.funifier/backups/` no `.gitignore` (o `init` adiciona automaticamente).
|
|
101
|
+
|
|
102
|
+
**Listar backups:**
|
|
103
|
+
```
|
|
104
|
+
funifier_permissions action=list_backups
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Restaurar (rollback):**
|
|
108
|
+
```
|
|
109
|
+
funifier_permissions action=restore_backup backup_path=<path retornado por list_backups>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
O restore valida que o `serverUrl` do snapshot coincide com a conexão atual — não aplique backup de outro tenant. O próprio restore também gera um snapshot prévio, permitindo desfazer o rollback.
|
|
113
|
+
|
|
114
|
+
Variáveis opcionais: `FUNIFIER_PROJECT_ROOT` (diretório do projeto; definida nos configs gerados pelo `init`) e `FUNIFIER_BACKUP_ROOT` (substitui `.funifier/backups/` por outro diretório). Snapshots não expiram automaticamente — remova arquivos antigos manualmente quando necessário.
|
|
@@ -74,8 +74,12 @@ If search returns insufficient results, read these directly:
|
|
|
74
74
|
| `point_category` | Tipos de ponto — `_id`, `category`, `shortName` |
|
|
75
75
|
| `catalog_item` | Loja virtual — `_id`, `catalogId`, `name`, `requires` |
|
|
76
76
|
|
|
77
|
+
Se o pipeline filtrar ou criar campos de data, leia também `references/date-handling.md` antes de montar o payload.
|
|
78
|
+
|
|
77
79
|
### Expressões de data Funifier
|
|
78
80
|
|
|
81
|
+
Para regras completas de criação/edição/consulta de datas via REST, triggers, schedulers, aggregate e prepared aggregate, leia `references/date-handling.md`.
|
|
82
|
+
|
|
79
83
|
| Expressão | Significado |
|
|
80
84
|
|---|---|
|
|
81
85
|
| `{"$date": "-0d-"}` | Início do dia atual |
|
|
@@ -84,6 +88,8 @@ If search returns insufficient results, read these directly:
|
|
|
84
88
|
| `{"$date": "-0y-"}` | Início do ano atual |
|
|
85
89
|
| `{"$date": "-1d-"}` | Início do dia anterior |
|
|
86
90
|
|
|
91
|
+
**Importante:** em aggregate direto e prepared aggregate, prefira expressões relativas Funifier dentro de `$date` (`-0M-`, `-0M+`) ou timestamps em milissegundos para datas absolutas. Para datas absolutas parametrizadas em prepared aggregate, normalize no `script` com `DateUtil.parse(...)` e substitua diretamente em `$gte`/`$lte`.
|
|
92
|
+
|
|
87
93
|
---
|
|
88
94
|
|
|
89
95
|
## Steps
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# funifier-date-handling
|
|
2
|
+
|
|
3
|
+
Create, edit, or query Date fields safely via REST, funifier_database, triggers, schedulers, aggregate, and prepared aggregate; use whenever a payload or filter contains dates
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Formato do payload (obrigatório)
|
|
8
|
+
|
|
9
|
+
Funifier armazena código como **strings JSON escapadas**. Ao montar o payload do `funifier_save`:
|
|
10
|
+
|
|
11
|
+
- **Quebra de linha** → `\n` (nunca newline literal dentro da string)
|
|
12
|
+
- **Aspas duplas** dentro do código → `\"`
|
|
13
|
+
- **Backslash** → `\\` (regex `\b` vira `\\b`; dentro de Groovy com string-building pode exigir `\\\\b`)
|
|
14
|
+
- **Tab** → `\t` (se usar)
|
|
15
|
+
- **Pipelines de aggregate** vão como **string JSON** (não objeto aninhado) — serialize o array inteiro
|
|
16
|
+
|
|
17
|
+
A regra prática: monte o objeto em JavaScript normalmente e passe por `JSON.stringify` — o MCP faz isso automaticamente quando você entrega o payload como JSON. **Nunca** cole código multilinha cru no meio do JSON.
|
|
18
|
+
|
|
19
|
+
### Exemplo 1 — custom-page (html + angularjs script)
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"title": "Ciclos",
|
|
24
|
+
"slug": "studio/custom/ciclos",
|
|
25
|
+
"html": "<div class=\"row\">\n <div class=\"col-md-12\">\n <h2>Ciclos</h2>\n </div>\n</div>\n",
|
|
26
|
+
"script": "$scope.all = [];\n$scope.loading = false;\n\n$scope.list = function () {\n $scope.loading = true;\n};\n$scope.list();\n"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Exemplo 2 — aggregate preparado (pipeline JSON + script Groovy)
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"_id": "extrato",
|
|
35
|
+
"title": "Extrato de Pontos",
|
|
36
|
+
"collection": "extrato",
|
|
37
|
+
"aggregate": "[\n {\n \"$match\": { \"player\": \"$param:player\" }\n },\n {\n \"$sort\": { \"time\": -1 }\n }\n]",
|
|
38
|
+
"script": "void prepare(aggregations, params) {\n if (context.get(\"player\") != null) {\n params.put(\"player\", context.get(\"player\"));\n } else {\n params.put(\"player\", \"NAO_EXISTE\");\n }\n}\n"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Note que `aggregate` é uma **string** contendo JSON serializado (com `\n` e `\"` escapados), **não** um array nativo. Mesma regra vale para `script` em triggers/schedulers/aggregates e `html`/`script` em custom-pages.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Before starting — find relevant docs
|
|
47
|
+
|
|
48
|
+
Use the `funifier_search_docs` MCP tool to load only what you need:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
funifier_search_docs "date dates bson extended-json aggregate prepared DateUtil strict timestamp timezone"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then read the most relevant results with `funifier_read_doc path=<path>`.
|
|
55
|
+
|
|
56
|
+
## Primary docs for this skill
|
|
57
|
+
|
|
58
|
+
If search returns insufficient results, read these directly:
|
|
59
|
+
|
|
60
|
+
- `funifier_read_doc path=guides/database-access`
|
|
61
|
+
- `funifier_read_doc path=guides/aggregates`
|
|
62
|
+
- `funifier_read_doc path=modules/database`
|
|
63
|
+
|
|
64
|
+
## Steps
|
|
65
|
+
|
|
66
|
+
## Regra de ouro
|
|
67
|
+
|
|
68
|
+
Use **BSON Extended JSON** para persistir datas em objetos customizados:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"startsAt": { "$date": "2026-05-28T13:30:00.000Z" },
|
|
73
|
+
"created": { "$date": "2026-05-28T13:30:00.000Z" }
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Evite salvar data como string ISO solta quando o campo será filtrado como data. String não compara corretamente com BSON Date em consultas Mongo.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Criar/editar datas via REST ou funifier_database
|
|
82
|
+
|
|
83
|
+
- Gere datas em UTC (`toISOString()` no JavaScript).
|
|
84
|
+
- Ao criar/editar documentos, envie campos de data como `{ "$date": "<ISO UTC>" }`.
|
|
85
|
+
- Em updates via `PUT /v3/database/<collection>`, reenvie o documento completo: o endpoint faz **full replace**.
|
|
86
|
+
- Use `strict=true` ao ler quando precisar confirmar tipos BSON preservados na resposta.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"_id": "cycle_2026_05",
|
|
91
|
+
"name": "Ciclo Maio",
|
|
92
|
+
"startsAt": { "$date": "2026-05-01T00:00:00.000Z" },
|
|
93
|
+
"endsAt": { "$date": "2026-05-31T23:59:59.999Z" }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Não salve assim se o campo será filtrado como data:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{ "startsAt": "2026-05-01T00:00:00.000Z" }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Triggers e schedulers
|
|
106
|
+
|
|
107
|
+
Scripts Groovy de triggers, schedulers e prepared aggregates já importam `DateUtil` e `JsonUtil` nos runners. Se o script precisa calcular/comparar datas, normalize explicitamente com `DateUtil.parse(...)`, `new Date()` ou `DateUtil.fromKeyword(...)`.
|
|
108
|
+
|
|
109
|
+
```groovy
|
|
110
|
+
void trigger(event, entity, player, database) {
|
|
111
|
+
entity.createdAt = new Date()
|
|
112
|
+
entity.startsAt = DateUtil.parse('2026-05-01T00:00:00.000Z')
|
|
113
|
+
entity.endsAt = DateUtil.fromKeyword('+7d+')
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Aggregate direto
|
|
120
|
+
|
|
121
|
+
Prefira períodos relativos com `$date`, que passam pelo `FunifierMarshaller` e são convertidos por `DateUtil.fromKeyword`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
"$match": {
|
|
127
|
+
"time": {
|
|
128
|
+
"$gte": { "$date": "-0M-" },
|
|
129
|
+
"$lte": { "$date": "-0M+" }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Não generalize para `q` de GET/list/count/delete: esses caminhos montam a query string diretamente.
|
|
137
|
+
|
|
138
|
+
Para data absoluta em aggregate direto, prefira timestamp em milissegundos dentro de `$date` ou teste o formato na instância antes de documentar como padrão.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Prepared aggregate (`/v3/find/<id>`)
|
|
143
|
+
|
|
144
|
+
Regras extras:
|
|
145
|
+
|
|
146
|
+
1. O campo `aggregate` salvo no recurso é uma **string JSON serializada**, não array nativo.
|
|
147
|
+
2. Parâmetros só são substituídos quando aparecem como string literal `"$param:nome"`.
|
|
148
|
+
|
|
149
|
+
Para datas relativas, use parâmetros dentro de `$date` (`"$date":"$param:start"`) e preencha `params.start` com keywords como `-0M-`.
|
|
150
|
+
|
|
151
|
+
Para datas absolutas, normalize no `script` para `Date` e substitua diretamente em `$gte`/`$lte`:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"aggregate": "[{"$match":{"soldAt":{"$gte":"$param:start","$lte":"$param:end"}}}]",
|
|
156
|
+
"script": "void prepare(aggregations, params) {\n Date start = params.startIso ? DateUtil.parse(params.startIso) : DateUtil.fromKeyword('-0M-')\n Date end = params.endIso ? DateUtil.parse(params.endIso) : DateUtil.fromKeyword('-0M+')\n if (start == null || end == null) throw new IllegalArgumentException('Invalid date params')\n params.put('start', start)\n params.put('end', end)\n}\n"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Não coloque o parâmetro convertido para `Date` dentro de `{ "$date": "$param:start" }`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Checklist
|
|
165
|
+
|
|
166
|
+
- [ ] Salvar Date como `{ "$date": "<ISO UTC>" }`, nunca string solta.
|
|
167
|
+
- [ ] Preservar todos os campos em `PUT`.
|
|
168
|
+
- [ ] Usar keywords Funifier (`-0d-`, `-0M-`, `-0M+`, `+1d+`) apenas em caminhos que passam pelo `FunifierMarshaller`.
|
|
169
|
+
- [ ] Em prepared aggregate, salvar `aggregate` como string JSON serializada.
|
|
170
|
+
- [ ] Em prepared aggregate, usar `"$param:nome"` exatamente como string.
|
|
171
|
+
- [ ] Para data absoluta em prepared aggregate, converter no `script` com `DateUtil.parse`, validar `null`, e substituir o `Date` diretamente em `$gte`/`$lte`.
|
|
172
|
+
- [ ] Testar aggregate direto antes de salvar como prepared aggregate.
|
|
@@ -47,6 +47,8 @@ If search returns insufficient results, read these directly:
|
|
|
47
47
|
| Challenge não avança | Action não existe ou operator/total errado | `funifier_list type=action search=<id>` |
|
|
48
48
|
| Compra falha sem erro | Saldo insuficiente ou `requires` errado | Checar ponto do jogador: `GET /v3/player/<id>/status` |
|
|
49
49
|
| `/v3/database` retorna vazio | Scope `database` ausente na role | Adicionar `database` ao scope via Security |
|
|
50
|
+
| Filtro por data não encontra registros | Campo foi salvo como string ISO ou parâmetro ISO foi usado dentro de `$date` sem normalização | Salvar como `{ "$date": "<ISO UTC>" }`; em prepared aggregate converter ISO com `DateUtil.parse` no script |
|
|
51
|
+
| Prepared aggregate com data absoluta falha/retorna vazio | `"$param:start"` dentro de `{ "$date": ... }` recebeu string ISO | Converter no `script` para `Date` e usar `"$gte":"$param:start"` diretamente |
|
|
50
52
|
|
|
51
53
|
---
|
|
52
54
|
|