@raclettejs/core 0.1.23 → 0.1.25

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/cli.js +2 -2
  3. package/dist/cli.js +136 -155
  4. package/dist/cli.js.map +4 -4
  5. package/dist/index.js +15 -16
  6. package/dist/index.js.map +4 -4
  7. package/package.json +24 -22
  8. package/services/backend/.yarn/install-state.gz +0 -0
  9. package/services/backend/dist/core/eventBus/index.js +7 -0
  10. package/services/backend/dist/core/pluginSystem/configGenerator/index.js +346 -0
  11. package/services/backend/dist/core/sockets/index.js +95 -0
  12. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/events/index.js +31 -0
  13. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/index.js +48 -0
  14. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/routes/index.js +15 -0
  15. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/events/index.js +22 -0
  16. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/index.js +51 -0
  17. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/routes/index.js +19 -0
  18. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/events/index.js +22 -0
  19. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/index.js +48 -0
  20. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.js +19 -0
  21. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/events/index.js +31 -0
  22. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/index.js +49 -0
  23. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/routes/index.js +16 -0
  24. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/events/index.js +39 -0
  25. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/index.js +50 -0
  26. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/routes/index.js +19 -0
  27. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/events/index.js +31 -0
  28. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/index.js +51 -0
  29. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/routes/index.js +21 -0
  30. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/events/index.js +31 -0
  31. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/index.js +48 -0
  32. package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.js +15 -0
  33. package/services/backend/dist/corePlugins/raclette__core/backend/index.js +34 -0
  34. package/services/backend/dist/domains/index.js +11 -0
  35. package/services/backend/dist/domains/system/index.js +11 -0
  36. package/services/backend/dist/domains/system/routes/index.js +17 -0
  37. package/services/backend/dist/helpers/index.js +14 -0
  38. package/services/backend/dist/index.js +3 -0
  39. package/services/backend/dist/modules/authentication/index.js +253 -0
  40. package/services/backend/dist/shared/types/core/index.js +8 -0
  41. package/services/backend/dist/shared/types/dataTypes/index.js +6 -0
  42. package/services/backend/dist/shared/types/index.js +8 -0
  43. package/services/backend/dist/shared/types/plugins/index.js +8 -0
  44. package/services/backend/dist/types/index.js +12 -0
  45. package/services/backend/dist/utils/index.js +2 -0
  46. package/services/backend/package.json +20 -18
  47. package/services/backend/src/core/contracting/contractRegistrar.ts +1 -1
  48. package/services/backend/src/core/pluginSystem/configGenerator/generatorTypes.ts +1 -0
  49. package/services/backend/src/core/pluginSystem/configGenerator/index.ts +2 -2
  50. package/services/backend/src/core/pluginSystem/pluginLoader.ts +99 -44
  51. package/services/backend/src/core/pluginSystem/pluginTypes.ts +1 -0
  52. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/index.ts +2 -0
  53. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchOwn.ts +68 -0
  54. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.model.ts +1 -1
  55. package/services/backend/src/corePlugins/raclette__core/frontend/generated-config.ts +12 -0
  56. package/services/backend/src/shared/schemas/core/Account.schema.ts +10 -10
  57. package/services/backend/src/shared/schemas/core/Composition.schema.ts +25 -22
  58. package/services/backend/src/shared/schemas/core/CompositionCreate.schema.ts +19 -16
  59. package/services/backend/src/shared/schemas/core/CompositionUpdate.schema.ts +19 -16
  60. package/services/backend/src/shared/schemas/core/InteractionLink.schema.ts +15 -15
  61. package/services/backend/src/shared/schemas/core/InteractionLinkCreate.schema.ts +9 -9
  62. package/services/backend/src/shared/schemas/core/InteractionLinkUpdate.schema.ts +9 -9
  63. package/services/backend/src/shared/schemas/core/Project.schema.ts +28 -6
  64. package/services/backend/src/shared/schemas/core/ProjectCreate.schema.ts +22 -0
  65. package/services/backend/src/shared/schemas/core/ProjectUpdate.schema.ts +25 -0
  66. package/services/backend/src/shared/schemas/core/Tag.schema.ts +11 -9
  67. package/services/backend/src/shared/schemas/core/TagCreate.schema.ts +1 -3
  68. package/services/backend/src/shared/schemas/core/TagUpdate.schema.ts +1 -0
  69. package/services/backend/src/shared/schemas/core/Trigger.schema.ts +3 -3
  70. package/services/backend/src/shared/schemas/core/User.schema.ts +18 -18
  71. package/services/backend/src/shared/schemas/core/UserCreate.schema.ts +9 -9
  72. package/services/backend/src/shared/schemas/core/Widget.schema.ts +5 -5
  73. package/services/backend/src/shared/schemas/core/WidgetLayout.schema.ts +9 -9
  74. package/services/backend/src/shared/types/core/Composition.types.ts +1 -0
  75. package/services/backend/src/shared/types/core/CompositionCreate.types.ts +1 -0
  76. package/services/backend/src/shared/types/core/CompositionUpdate.types.ts +1 -0
  77. package/services/backend/src/shared/types/core/Project.types.ts +4 -0
  78. package/services/backend/src/shared/types/core/ProjectCreate.types.ts +4 -0
  79. package/services/backend/src/shared/types/core/ProjectUpdate.types.ts +4 -1
  80. package/services/backend/src/shared/types/core/Tag.types.ts +1 -1
  81. package/services/backend/src/shared/types/core/TagCreate.types.ts +0 -1
  82. package/services/backend/src/shared/types/core/index.ts +4 -0
  83. package/services/backend/src/shared/types/plugins/index.ts +1 -1
  84. package/services/backend/tsconfig.json +1 -0
  85. package/services/backend/yarn.lock +797 -935
  86. package/services/frontend/.yarn/install-state.gz +0 -0
  87. package/services/frontend/eslint.config.js +9 -0
  88. package/services/frontend/index.html +3 -3
  89. package/services/frontend/package.json +30 -28
  90. package/services/frontend/src/core/setup/plugin-system/discovery/app-plugins.ts +43 -4
  91. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +10 -11
  92. package/services/frontend/src/orchestrator/assets/styles/layers.css +11 -0
  93. package/services/frontend/src/orchestrator/assets/styles/tailwindStyles.css +5 -11
  94. package/services/frontend/src/orchestrator/assets/styles/vuetifyStyles.scss +0 -4
  95. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +5 -18
  96. package/services/frontend/src/orchestrator/composables/useWidgets/helperFunctions.ts +54 -5
  97. package/services/frontend/src/orchestrator/helpers/index.ts +1 -0
  98. package/services/frontend/src/orchestrator/helpers/widgetVisuals.ts +71 -0
  99. package/services/frontend/src/orchestrator/setup/vuetify.ts +2 -0
  100. package/services/frontend/src/orchestrator/types/Widgets.ts +5 -1
  101. package/services/frontend/vite.config.ts +24 -10
  102. package/services/frontend/yarn.lock +0 -5224
@@ -0,0 +1,17 @@
1
+ import { getBlockedLogins, resetLoginBlock, resetAllLoginBlocks, } from "./route.admin.cache";
2
+ import deploy from "./route.deploy";
3
+ import login from "./route.user.login";
4
+ import checkToken from "./route.user.checkToken";
5
+ import getAllCollections from "./route.collection.getAll";
6
+ import deleteCollections from "./route.collection.remove";
7
+ const registerRoutes = async (fastify) => {
8
+ await fastify.get("/system/deploy", deploy(fastify));
9
+ await fastify.post("/user/login", login(fastify));
10
+ await fastify.get("/user/check-token", checkToken(fastify));
11
+ await fastify.get("/admin/cache/blocks", getBlockedLogins(fastify));
12
+ await fastify.delete("/admin/cache/blocks/:key", resetLoginBlock(fastify));
13
+ await fastify.delete("/admin/cache/blocks", resetAllLoginBlocks(fastify));
14
+ await fastify.get("/admin/collections/all", getAllCollections(fastify));
15
+ await fastify.delete("/admin/collections", deleteCollections(fastify));
16
+ };
17
+ export default registerRoutes;
@@ -0,0 +1,14 @@
1
+ import { customAlphabet } from "nanoid";
2
+ export * from "./cors";
3
+ const customCyperAlphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
4
+ /**
5
+ * Random ID generator
6
+ *
7
+ * @param length - Required length for the output
8
+ * @returns Function that generates random IDs
9
+ * @example
10
+ * const generateId = randomIdGenerator(12);
11
+ * const id = generateId(); // => 'gdh6326GDJh0'
12
+ */
13
+ const randomIdGenerator = (length = 16) => customAlphabet(customCyperAlphabet, length);
14
+ export { randomIdGenerator };
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./utils";
3
+ export * from "./core/contracting/contractRegistrar";
@@ -0,0 +1,253 @@
1
+ import jwt from "@fastify/jwt";
2
+ import fastifyPlugin from "fastify-plugin";
3
+ import configService from "@c/config/configService";
4
+ import { ForbiddenError } from "@/utils/errors";
5
+ const WORK_SESSION_COLLECTION = "raclette__core-worksession";
6
+ const WORK_SESSION_HEADER = "x-work-session-id";
7
+ /**
8
+ * Middleware: allow either authenticated user (JWT) or valid work session (X-Work-Session-Id).
9
+ * When using this, the route states that it handles permission checks itself (e.g. compare req.workSession with query).
10
+ * - If JWT present and valid: sets req.user (normal auth).
11
+ * - If no/invalid JWT but X-Work-Session-Id present: loads work session, validates (active, not expired),
12
+ * optionally checks that workSession.pluginData includes the given pluginKey, then sets req.workSession and req.isWorkSession.
13
+ */
14
+ export const authenticatedOrWorksession = async (req, res, fastify, pluginKey) => {
15
+ fastify.log.debug(`[Auth] authenticatedOrWorksession: ${pluginKey} ${req.method} ${req.url}`);
16
+ const { requireAuthentication } = configService.getConfig()?.global ?? {
17
+ requireAuthentication: true,
18
+ };
19
+ const isAdminTagRequired = configService.isAdminTagRequired();
20
+ if (!requireAuthentication) {
21
+ const mockUser = { _id: "SYSTEM" };
22
+ req.user = mockUser;
23
+ return;
24
+ }
25
+ // 1) Try JWT first
26
+ try {
27
+ await req.jwtVerify();
28
+ const payload = req.user;
29
+ fastify.log.debug(`[Auth] JWT verified, payload type: ${payload.type || "user"}, sessionId: ${payload.sessionId || "none"}, _id: ${payload._id || "none"}`);
30
+ if (payload.type === "workSession" && payload.sessionId) {
31
+ fastify.log.info(`[Auth] Work session JWT detected, sessionId: ${payload.sessionId}, pluginKey: ${pluginKey}`);
32
+ // Work session JWT: load session and validate
33
+ const workSession = await loadAndValidateWorkSession(fastify, payload.sessionId, pluginKey);
34
+ if (!workSession) {
35
+ fastify.log.warn(`[Auth] Work session validation failed for sessionId: ${payload.sessionId}`);
36
+ return res
37
+ .status(401)
38
+ .send({ message: "Invalid or expired work session" });
39
+ }
40
+ fastify.log.info(`[Auth] Work session authenticated: sessionId=${payload.sessionId}, pluginKey=${pluginKey}`);
41
+ req.workSession = workSession;
42
+ req.isWorkSession = true;
43
+ // Set projectId from workSession.project if available
44
+ if (workSession.project) {
45
+ req.projectId = workSession.project;
46
+ fastify.log.debug(`[Auth] Work session projectId: ${req.projectId}`);
47
+ }
48
+ return;
49
+ }
50
+ // Normal user JWT: fetch user and set req.user
51
+ fastify.log.debug(`[Auth] Normal user JWT, fetching user: ${req.user?._id}`);
52
+ const { _id } = req.user;
53
+ const adminTag = await fastify.database
54
+ .collection("raclette__core-tag")
55
+ .findOne({ title: "ADMIN", locked: true, type: "system" });
56
+ let isAdmin = false;
57
+ const user = await fastify.database
58
+ .collection("raclette__core-user")
59
+ .findOne({ _id }, { projection: { password: 0, resetKey: 0 } });
60
+ if (user && user.tags && adminTag) {
61
+ user.tags.forEach((tag) => {
62
+ if (adminTag._id === tag)
63
+ isAdmin = true;
64
+ });
65
+ }
66
+ if (isAdminTagRequired && !isAdmin) {
67
+ fastify.log.error("User not allowed in workbench");
68
+ return res.status(403).send({ message: "User not allowed in workbench" });
69
+ }
70
+ if (isAdmin)
71
+ user.isAdmin = true;
72
+ if (!user) {
73
+ fastify.log.error("User not found");
74
+ return res.status(401).send({ message: "User not found" });
75
+ }
76
+ const account = await fastify.database
77
+ .collection("raclette__core-account")
78
+ .findOne({ _id: user.account });
79
+ user.account = account;
80
+ req.user = user;
81
+ // Set projectId from user's lastProjectId (for requestData.project in payload wrappers)
82
+ if (user.ux?.lastProjectId) {
83
+ req.projectId = user.ux.lastProjectId;
84
+ fastify.log.debug(`[Auth] User projectId: ${req.projectId}`);
85
+ }
86
+ return;
87
+ }
88
+ catch (error) {
89
+ // JWT missing or invalid: try work session via header
90
+ fastify.log.debug(`[Auth] JWT verification failed: ${error.message}, trying work session header`);
91
+ }
92
+ // 2) Try X-Work-Session-Id header
93
+ const sessionId = req.headers[WORK_SESSION_HEADER];
94
+ if (sessionId) {
95
+ const workSession = await loadAndValidateWorkSession(fastify, sessionId, pluginKey);
96
+ if (workSession) {
97
+ req.workSession = workSession;
98
+ req.isWorkSession = true;
99
+ // Set projectId from workSession.project if available
100
+ // workaround for how projects/spaces are currently handled - tbd
101
+ if (workSession.project) {
102
+ req.projectId = workSession.project;
103
+ }
104
+ return;
105
+ }
106
+ else {
107
+ fastify.log.warn(`[Auth] Work session validation failed for header sessionId: ${sessionId}`);
108
+ }
109
+ }
110
+ return res.status(401).send({ message: "Authentication required" });
111
+ };
112
+ async function loadAndValidateWorkSession(fastify, sessionId, pluginKey) {
113
+ fastify.log.debug(`[WorkSession Auth] Loading and validating work session: sessionId=${sessionId}, pluginKey=${pluginKey}`);
114
+ if (!sessionId || typeof sessionId !== "string") {
115
+ fastify.log.warn(`[WorkSession Auth] Invalid sessionId: ${sessionId}`);
116
+ return null;
117
+ }
118
+ fastify.log.debug(`[WorkSession Auth] Querying database for work session`);
119
+ const doc = await fastify.database
120
+ .collection(WORK_SESSION_COLLECTION)
121
+ .findOne({ _id: sessionId });
122
+ if (!doc) {
123
+ fastify.log.warn(`[WorkSession Auth] Work session not found: ${sessionId}`);
124
+ return null;
125
+ }
126
+ if (!doc.isActive) {
127
+ fastify.log.warn(`[WorkSession Auth] Work session is not active: ${sessionId}`);
128
+ return null;
129
+ }
130
+ const expiry = doc.expiryDate ? new Date(doc.expiryDate) : null;
131
+ if (expiry && expiry.getTime() < Date.now()) {
132
+ fastify.log.warn(`[WorkSession Auth] Work session expired: ${sessionId}, expiryDate: ${expiry.toISOString()}`);
133
+ return null;
134
+ }
135
+ // Check that this plugin is part of the work session (pluginData keys are "pluginKey:widgetName")
136
+ const pluginData = doc.pluginData || {};
137
+ const pluginKeys = Object.keys(pluginData);
138
+ const hasPlugin = pluginKeys.some((k) => k.startsWith(pluginKey + ":")) ||
139
+ Object.prototype.hasOwnProperty.call(pluginData, pluginKey);
140
+ if (!hasPlugin) {
141
+ fastify.log.warn(`[WorkSession Auth] Plugin ${pluginKey} not found in work session pluginData: ${pluginKeys.join(", ")}`);
142
+ return null;
143
+ }
144
+ fastify.log.info(`[WorkSession Auth] Work session validated successfully: sessionId=${sessionId}, pluginKey=${pluginKey}`);
145
+ return doc;
146
+ }
147
+ export const authenticate = async (req, res, fastify) => {
148
+ {
149
+ const { requireAuthentication } = configService.getConfig()?.global ?? {
150
+ requireAuthentication: true,
151
+ };
152
+ const isAdminTagRequired = configService.isAdminTagRequired();
153
+ // Skip JWT verification and database queries, if global.requireAuthentication is configured to false
154
+ if (!requireAuthentication) {
155
+ const mockUser = {
156
+ _id: "SYSTEM",
157
+ };
158
+ req.user = mockUser;
159
+ return;
160
+ }
161
+ try {
162
+ await req.jwtVerify();
163
+ const payload = req.user;
164
+ if (payload.type === "workSession") {
165
+ return res
166
+ .status(401)
167
+ .send({ message: "Work session token not allowed on this route" });
168
+ }
169
+ // fetch current user data from the db
170
+ const { _id } = req.user;
171
+ const adminTag = await fastify.database
172
+ .collection("raclette__core-tag")
173
+ .findOne({ title: "ADMIN", locked: true, type: "system" });
174
+ // IMPORTANT
175
+ // due to some very weird behaviour, mongoose always returns empty results
176
+ // so we are using mongodb directly until we fix this
177
+ let isAdmin = false;
178
+ const user = await fastify.database
179
+ .collection("raclette__core-user")
180
+ .findOne({ _id }, { projection: { password: 0, resetKey: 0 } });
181
+ if (user && user.tags && adminTag) {
182
+ user.tags.forEach((tag) => {
183
+ if (adminTag._id === tag) {
184
+ isAdmin = true;
185
+ }
186
+ });
187
+ }
188
+ if (isAdminTagRequired && !isAdmin) {
189
+ fastify.log.error("User not allowed in workbench");
190
+ throw new Error("User not allowed in workbench");
191
+ }
192
+ if (isAdmin) {
193
+ user.isAdmin = true;
194
+ }
195
+ if (!user) {
196
+ fastify.log.error("User not found");
197
+ throw new Error("User not found");
198
+ }
199
+ const account = await fastify.database
200
+ .collection("raclette__core-account")
201
+ .findOne({ _id: user.account });
202
+ user.account = account;
203
+ req.user = user;
204
+ }
205
+ catch (error) {
206
+ return res.send(error);
207
+ }
208
+ }
209
+ };
210
+ export const checkPermissions = async (req, res, fastify, permissions) => {
211
+ {
212
+ try {
213
+ if (!permissions.length) {
214
+ fastify.log.error("No permissions given for permission check");
215
+ throw new Error("No Permissions");
216
+ }
217
+ // TODO implement proper Permission lookup here
218
+ permissions.forEach((permissionStr) => {
219
+ if (permissionStr === "user.isAdmin") {
220
+ if (!req.user.isAdmin) {
221
+ throw new ForbiddenError("User not permitted to action");
222
+ }
223
+ }
224
+ });
225
+ }
226
+ catch (error) {
227
+ fastify.log.error(error.message);
228
+ return res.send(error.message);
229
+ }
230
+ }
231
+ };
232
+ export default fastifyPlugin(async (fastify, opts = {}) => {
233
+ // provide some sensible options
234
+ const defaults = {
235
+ secret: false,
236
+ decode: { complete: true },
237
+ sign: {
238
+ iss: "api.raclettejs.info",
239
+ expiresIn: "30d",
240
+ },
241
+ verify: { allowedIss: "api.raclettejs.info" },
242
+ };
243
+ // merge proved opts with defaults
244
+ const mergedOptions = { ...defaults, ...opts };
245
+ /* register the plugin */
246
+ fastify.register(jwt, mergedOptions);
247
+ // decorate the fastify instance, so we can call it anywhere
248
+ fastify.decorate("authenticate", (req, res) => {
249
+ return authenticate(req, res, fastify);
250
+ });
251
+ fastify.decorate("authenticatedOrWorksession", (pluginKey) => (req, res) => authenticatedOrWorksession(req, res, fastify, pluginKey));
252
+ fastify.decorate("checkPermissions", (permissionsArr) => (req, res) => checkPermissions(req, res, fastify, permissionsArr));
253
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ⚠️ WARNING: THIS FILE IS AUTO-GENERATED ⚠️
3
+ *
4
+ * This file is generated by the type generation system.
5
+ * Any manual changes will be overwritten on next server start and should always be committed.
6
+ *
7
+ */
8
+ export {};
@@ -0,0 +1,6 @@
1
+ export * from "./tags";
2
+ // TODO make the mongoDataTypes dynamic
3
+ export const systemDataTypes = ["account", "projectConfig", "userConfig"];
4
+ export const userContents = ["tag", "user", "project"];
5
+ export const mongoDataTypes = systemDataTypes.concat(userContents);
6
+ export const syncableDatatypes = ["tag", "user", "searchResult"];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ⚠️ WARNING: THIS FILE IS AUTO-GENERATED ⚠️
3
+ *
4
+ * This file is generated by the type generation system.
5
+ * Any manual changes will be overwritten on next server start and should always be committed.
6
+ *
7
+ */
8
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ⚠️ WARNING: THIS FILE IS AUTO-GENERATED ⚠️
3
+ *
4
+ * This file is generated by the type generation system.
5
+ * Any manual changes will be overwritten on next server start and should always be committed.
6
+ *
7
+ */
8
+ export {};
@@ -0,0 +1,12 @@
1
+ export * from "../core/backgroundTasks/taskManagerTypes";
2
+ export * from "../core/config/configTypes";
3
+ export * from "../core/crud/crudTypes";
4
+ export * from "../core/http/httpTypes";
5
+ export * from "../core/http/httpTypes";
6
+ export * from "../core/payload/payloadTypes";
7
+ export * from "../core/pluginSystem/pluginTypes";
8
+ export * from "../core/sockets/socketTypes";
9
+ export * from "../core/validation/schemaTypes";
10
+ export * from "../modules/cache/cacheTypes";
11
+ export * from "../core/validation/schemaTypes";
12
+ export * from "../core/pluginSystem/configGenerator/generatorTypes";
@@ -0,0 +1,2 @@
1
+ export * from "./errors";
2
+ export * from "./request.utils";
@@ -16,6 +16,7 @@
16
16
  "build:assets:src": "copyfiles -u 1 'src/**/*.{json,js,yml,yaml,env*}' dist",
17
17
  "build:assets:root": "copyfiles -u 0 'raclette.config.js' dist",
18
18
  "build:assets": "npm run build:assets:src && npm run build:assets:root",
19
+ "lint": "eslint .",
19
20
  "migrate:create": "npx migrate-mongo create -f './mongo-migrations/migrate-mongo-config.js'",
20
21
  "migrate:up": "migrate-mongo up -f './mongo-migrations/migrate-mongo-config.js'",
21
22
  "migrate:down": "migrate-mongo down -f './mongo-migrations/migrate-mongo-config.js'",
@@ -24,28 +25,29 @@
24
25
  "typecheck:watch": "tsc --noEmit --watch"
25
26
  },
26
27
  "dependencies": {
27
- "@fastify/cors": "11.1.0",
28
+ "@fastify/cors": "11.2.0",
28
29
  "@fastify/jwt": "10.0.0",
29
30
  "@fastify/rate-limit": "10.3.0",
30
31
  "@fastify/schedule": "6.0.0",
31
- "@fastify/sensible": "6.0.3",
32
- "@sinclair/typebox": "0.34.41",
32
+ "@fastify/sensible": "6.0.4",
33
+ "@sinclair/typebox": "0.34.48",
33
34
  "@types/vary": "1.1.3",
34
- "axios": "1.13.2",
35
+ "axios": "1.13.6",
35
36
  "bcrypt": "6.0.0",
36
37
  "copyfiles": "2.4.1",
37
- "fastify": "5.6.0",
38
+ "fastify": "5.8.2",
38
39
  "fastify-custom-healthcheck": "4.0.0",
39
- "glob": "11.0.3",
40
+ "fs-extra": "11.3.4",
41
+ "glob": "13.0.6",
40
42
  "iovalkey": "0.3.3",
41
- "migrate-mongo": "12.1.3",
42
- "mongodb": "^6.20.0",
43
- "mongoose": "8.18.1",
44
- "nanoid": "5.1.5",
45
- "pino": "9.9.5",
46
- "pino-pretty": "13.1.1",
47
- "ramda": "0.31.3",
48
- "socket.io": "4.8.1",
43
+ "migrate-mongo": "14.0.7",
44
+ "mongodb": "6.21.0",
45
+ "mongoose": "8.18.3",
46
+ "nanoid": "5.1.7",
47
+ "pino": "10.3.1",
48
+ "pino-pretty": "13.1.3",
49
+ "ramda": "0.32.0",
50
+ "socket.io": "4.8.3",
49
51
  "toad-scheduler": "3.1.0",
50
52
  "tsc-alias": "1.8.16",
51
53
  "uuid": "13.0.0"
@@ -58,9 +60,9 @@
58
60
  "@types/uuid": "11.0.0",
59
61
  "eslint": "9.35.0",
60
62
  "eslint-plugin-import": "2.32.0",
61
- "eslint-plugin-prefer-arrow-functions": "3.8.1",
62
- "tsx": "4.20.5",
63
- "typescript": "5.9.2",
64
- "typescript-eslint": "8.44.0"
63
+ "eslint-plugin-prefer-arrow-functions": "3.9.1",
64
+ "tsx": "4.21.0",
65
+ "typescript": "5.9.3",
66
+ "typescript-eslint": "8.57.1"
65
67
  }
66
68
  }
@@ -12,7 +12,7 @@ export const registerPluginContract = (
12
12
  contract: PluginContract,
13
13
  ): void => {
14
14
  if (contracts[pluginKey]) {
15
- throw new Error(`Plugin contract for "${pluginKey}" is already registered`)
15
+ throw new Error(`Plugin contract for "${pluginKey}" is already registered. Make sure that each plugin is calling fastify.generatePluginContract only once! Multiple calls are not allowed.`)
16
16
  }
17
17
 
18
18
  contracts[pluginKey] = contract
@@ -13,6 +13,7 @@ export type PluginRegistration = {
13
13
  author: string
14
14
  pluginKey: string
15
15
  pluginPath: string
16
+ frontendPath: string
16
17
  routePrefix: string
17
18
  entityMapping?: Record<string, string>
18
19
  customConfig?: {
@@ -42,6 +42,7 @@ class FrontendConfigRegistry {
42
42
  author: pluginData.author,
43
43
  pluginKey: pluginData.key,
44
44
  pluginPath: pluginData.absolutePath,
45
+ frontendPath: pluginData.frontendAbsolutePath,
45
46
  routePrefix: pluginData.routePrefix,
46
47
  entityMapping: options?.entityMapping || {},
47
48
  customConfig: options?.customConfig,
@@ -451,8 +452,7 @@ const writePluginConfigFile = async (
451
452
 
452
453
  const configContent = generatePluginConfigFile(dataTypes, registration)
453
454
  const outputPath = path.join(
454
- registration.pluginPath,
455
- "frontend",
455
+ registration.frontendPath,
456
456
  "generated-config.ts",
457
457
  )
458
458
 
@@ -30,6 +30,7 @@ interface PluginInfo {
30
30
  pluginName: string
31
31
  modulePath: string
32
32
  pluginPath: string
33
+ frontendPath: string
33
34
  type: PluginType
34
35
  metadata: PluginMetadata
35
36
  }
@@ -75,6 +76,74 @@ const logPluginError = (
75
76
  fastify.log.debug(`${createLogPrefix(type)} ${identifier}: ERROR - ${error}`)
76
77
  }
77
78
 
79
+ const normalizeRelativeDir = (dir?: string): string | undefined =>
80
+ dir?.replace(/^\.\//, "")
81
+
82
+ const resolveExistingServicePath = async (
83
+ pluginPath: string,
84
+ candidates: string[],
85
+ ): Promise<{ relativePath: string; absolutePath: string } | undefined> => {
86
+ for (const candidatePath of candidates) {
87
+ const normalizedCandidatePath = normalizeRelativeDir(candidatePath)
88
+ if (!normalizedCandidatePath) continue
89
+
90
+ const candidateAbsolutePath = path.join(pluginPath, normalizedCandidatePath)
91
+ try {
92
+ const stat = await fs.promises.stat(candidateAbsolutePath)
93
+ if (stat.isDirectory()) {
94
+ return {
95
+ relativePath: normalizedCandidatePath,
96
+ absolutePath: candidateAbsolutePath,
97
+ }
98
+ }
99
+ } catch {
100
+ // path missing or not accessible
101
+ }
102
+ }
103
+ }
104
+
105
+ const resolveBackendPath = async (
106
+ pluginPath: string,
107
+ metadata: PluginMetadata,
108
+ ): Promise<{ relativePath: string; absolutePath: string } | undefined> => {
109
+ const backendPathCandidates = getBackendPathCandidates(metadata)
110
+
111
+ return resolveExistingServicePath(pluginPath, backendPathCandidates)
112
+ }
113
+
114
+ const getBackendPathCandidates = (metadata: PluginMetadata): string[] => {
115
+ const explicitBackendDir = normalizeRelativeDir(metadata.backendDir)
116
+ if (explicitBackendDir) {
117
+ return [explicitBackendDir]
118
+ }
119
+
120
+ return ["backend", "src/backend"]
121
+ }
122
+
123
+ const resolveFrontendPath = async (
124
+ pluginPath: string,
125
+ metadata: PluginMetadata,
126
+ backendRelativePath?: string,
127
+ ): Promise<string> => {
128
+ const explicitFrontendDir = normalizeRelativeDir(metadata.frontendDir)
129
+ if (explicitFrontendDir) {
130
+ return path.join(pluginPath, explicitFrontendDir)
131
+ }
132
+
133
+ const resolvedFrontendPath = await resolveExistingServicePath(pluginPath, [
134
+ "frontend",
135
+ "src/frontend",
136
+ ])
137
+ if (resolvedFrontendPath) {
138
+ return resolvedFrontendPath.absolutePath
139
+ }
140
+
141
+ // Fall back to src/frontend when backend is under src, otherwise frontend.
142
+ const fallbackRelativePath =
143
+ backendRelativePath?.startsWith("src/") ? "src/frontend" : "frontend"
144
+ return path.join(pluginPath, fallbackRelativePath)
145
+ }
146
+
78
147
  /**
79
148
  * Get app plugin paths for current environment
80
149
  */
@@ -254,16 +323,18 @@ const collectLocalPluginsFromPath = async (
254
323
  continue
255
324
  }
256
325
 
257
- // Get backend path from metadata or use default
258
- const backendRelativePath = metadata.backendDir || "backend"
259
- const backendPath = path.join(pluginPath, backendRelativePath)
326
+ // Get backend path from metadata or try supported defaults.
327
+ const resolvedBackendPath = await resolveBackendPath(pluginPath, metadata)
328
+ const backendRelativePath = resolvedBackendPath?.relativePath
329
+ const backendPath = resolvedBackendPath?.absolutePath
260
330
 
261
- if (!fs.existsSync(backendPath)) {
331
+ if (!backendRelativePath || !backendPath) {
332
+ const backendPathCandidates = getBackendPathCandidates(metadata)
262
333
  logPluginSkipped(
263
334
  fastify,
264
335
  pluginType,
265
336
  pluginIdentifier,
266
- `backend directory not found: ${backendRelativePath}`,
337
+ `backend directory not found: ${backendPathCandidates.join(" or ")}`,
267
338
  )
268
339
  if (metadata.backendDir) {
269
340
  // we only cancel when the dir was explicitly set
@@ -277,24 +348,6 @@ const collectLocalPluginsFromPath = async (
277
348
  continue
278
349
  }
279
350
 
280
- if (!fs.statSync(backendPath).isDirectory()) {
281
- logPluginSkipped(
282
- fastify,
283
- pluginType,
284
- pluginIdentifier,
285
- `backend path is not a directory: ${backendRelativePath}`,
286
- )
287
- if (metadata.backendDir) {
288
- handlePluginError(
289
- fastify,
290
- `Server directory not found for plugin: ${configPath}`,
291
- pluginType,
292
- errorFlags,
293
- )
294
- }
295
- continue
296
- }
297
-
298
351
  // Find the main module file
299
352
  const modulePath = getFilePathWithExtension(backendPath, "index")
300
353
  if (!modulePath) {
@@ -315,12 +368,18 @@ const collectLocalPluginsFromPath = async (
315
368
 
316
369
  const author = slugify(metadata.author)
317
370
  const pluginName = slugify(metadata.name)
371
+ const frontendPath = await resolveFrontendPath(
372
+ pluginPath,
373
+ metadata,
374
+ backendRelativePath,
375
+ )
318
376
 
319
377
  plugins.push({
320
378
  author,
321
379
  pluginName,
322
380
  modulePath,
323
381
  pluginPath,
382
+ frontendPath,
324
383
  type: pluginType,
325
384
  metadata,
326
385
  })
@@ -462,32 +521,21 @@ const collectNpmPlugins = async (
462
521
  continue
463
522
  }
464
523
 
465
- // Get backend path from metadata or use default
466
- const backendRelativePath = pluginMetadata.backendDir || "src/backend"
467
- const backendPath = path.join(pluginPath, backendRelativePath)
468
-
469
- if (!fs.existsSync(backendPath)) {
470
- logPluginSkipped(
471
- fastify,
472
- PluginType.NPM,
473
- pluginPackage,
474
- `backend directory not found: ${backendRelativePath}`,
475
- )
476
- handlePluginError(
477
- fastify,
478
- `Server directory not found for npm plugin: ${pluginPackage}`,
479
- PluginType.NPM,
480
- errorFlags,
481
- )
482
- continue
483
- }
524
+ // Get backend path from metadata or try supported defaults.
525
+ const resolvedBackendPath = await resolveBackendPath(
526
+ pluginPath,
527
+ pluginMetadata,
528
+ )
529
+ const backendRelativePath = resolvedBackendPath?.relativePath
530
+ const backendPath = resolvedBackendPath?.absolutePath
484
531
 
485
- if (!fs.statSync(backendPath).isDirectory()) {
532
+ if (!backendRelativePath || !backendPath) {
533
+ const backendPathCandidates = getBackendPathCandidates(pluginMetadata)
486
534
  logPluginSkipped(
487
535
  fastify,
488
536
  PluginType.NPM,
489
537
  pluginPackage,
490
- `backend path is not a directory: ${backendRelativePath}`,
538
+ `backend directory not found: ${backendPathCandidates.join(" or ")}`,
491
539
  )
492
540
  handlePluginError(
493
541
  fastify,
@@ -518,12 +566,18 @@ const collectNpmPlugins = async (
518
566
 
519
567
  const author = slugify(pluginMetadata.author)
520
568
  const pluginName = slugify(pluginMetadata.name)
569
+ const frontendPath = await resolveFrontendPath(
570
+ pluginPath,
571
+ pluginMetadata,
572
+ backendRelativePath,
573
+ )
521
574
 
522
575
  plugins.push({
523
576
  author,
524
577
  pluginName,
525
578
  modulePath,
526
579
  pluginPath,
580
+ frontendPath,
527
581
  type: PluginType.NPM,
528
582
  metadata: pluginMetadata,
529
583
  })
@@ -704,6 +758,7 @@ const registerCollectedPlugins = async (
704
758
  collectionPrefix,
705
759
  routePrefix,
706
760
  absolutePath: plugin.pluginPath,
761
+ frontendAbsolutePath: plugin.frontendPath,
707
762
  }
708
763
 
709
764
  // Dynamically import the plugin module