@oondemand/oon-core-back 0.3.74 → 0.3.76

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.
@@ -4,22 +4,13 @@ const fs = require("node:fs");
4
4
  const path = require("node:path");
5
5
  const ctx = require("../core/context");
6
6
  const corePackage = require("../../package.json");
7
+ const { validateRbac } = require("./rbacManifest");
7
8
 
8
9
  const APP_MANIFEST_FILENAME = "central.app.json";
9
10
  const APP_MANIFEST_SCHEMA_VERSION = 1;
10
- const SUPPORTED_APP_KINDS = Object.freeze([
11
- "root-central",
12
- "member-central",
13
- "portal-cockpit",
14
- ]);
11
+ const SUPPORTED_APP_KINDS = Object.freeze(["root-central", "member-central", "portal-cockpit"]);
15
12
  const SUPPORTED_MODULES = Object.freeze([
16
- "collections",
17
- "documents",
18
- "pipelines",
19
- "integrations",
20
- "omie",
21
- "assistants",
22
- "currencies",
13
+ "collections", "documents", "pipelines", "integrations", "omie", "assistants", "currencies",
23
14
  ]);
24
15
  const APP_KIND_ROLE = Object.freeze({
25
16
  "root-central": "root",
@@ -41,22 +32,17 @@ class AppManifestError extends Error {
41
32
  function isPlainObject(value) {
42
33
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
43
34
  }
44
-
45
35
  function hasOwn(object, key) {
46
36
  return Object.prototype.hasOwnProperty.call(object, key);
47
37
  }
48
-
49
38
  function addIssue(issues, fieldPath, message) {
50
39
  issues.push({ path: fieldPath, message });
51
40
  }
52
-
53
41
  function parseVersion(value) {
54
42
  if (typeof value !== "string") return null;
55
43
  const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/);
56
- if (!match) return null;
57
- return match.slice(1, 4).map(Number);
44
+ return match ? match.slice(1, 4).map(Number) : null;
58
45
  }
59
-
60
46
  function compareVersions(left, right) {
61
47
  for (let index = 0; index < 3; index += 1) {
62
48
  if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
@@ -73,50 +59,28 @@ function validateCompatibility(compatibility, issues, coreVersion) {
73
59
  addIssue(issues, "compatibility.core", "deve ser um objeto.");
74
60
  return;
75
61
  }
76
-
77
62
  const allowedKeys = new Set(["minVersion", "maxVersionExclusive"]);
78
63
  for (const key of Object.keys(compatibility.core)) {
79
64
  if (!allowedKeys.has(key)) addIssue(issues, `compatibility.core.${key}`, "propriedade não suportada.");
80
65
  }
81
-
82
66
  const minVersion = parseVersion(compatibility.core.minVersion);
83
67
  const maxVersion = hasOwn(compatibility.core, "maxVersionExclusive")
84
68
  ? parseVersion(compatibility.core.maxVersionExclusive)
85
69
  : null;
86
-
87
- if (!minVersion) {
88
- addIssue(issues, "compatibility.core.minVersion", "deve usar versão semântica, por exemplo 0.3.45.");
89
- }
70
+ if (!minVersion) addIssue(issues, "compatibility.core.minVersion", "deve usar versão semântica, por exemplo 0.3.45.");
90
71
  if (hasOwn(compatibility.core, "maxVersionExclusive") && !maxVersion) {
91
- addIssue(
92
- issues,
93
- "compatibility.core.maxVersionExclusive",
94
- "deve usar versão semântica, por exemplo 0.4.0.",
95
- );
72
+ addIssue(issues, "compatibility.core.maxVersionExclusive", "deve usar versão semântica, por exemplo 0.4.0.");
96
73
  }
97
74
  if (minVersion && maxVersion && compareVersions(minVersion, maxVersion) >= 0) {
98
- addIssue(
99
- issues,
100
- "compatibility.core",
101
- "minVersion deve ser menor que maxVersionExclusive.",
102
- );
75
+ addIssue(issues, "compatibility.core", "minVersion deve ser menor que maxVersionExclusive.");
103
76
  }
104
-
105
77
  const installedVersion = parseVersion(coreVersion);
106
78
  if (!installedVersion || !minVersion) return;
107
79
  if (compareVersions(installedVersion, minVersion) < 0) {
108
- addIssue(
109
- issues,
110
- "compatibility.core.minVersion",
111
- `requer OonCore ${compatibility.core.minVersion} ou superior; instalado ${coreVersion}.`,
112
- );
80
+ addIssue(issues, "compatibility.core.minVersion", `requer OonCore ${compatibility.core.minVersion} ou superior; instalado ${coreVersion}.`);
113
81
  }
114
82
  if (maxVersion && compareVersions(installedVersion, maxVersion) >= 0) {
115
- addIssue(
116
- issues,
117
- "compatibility.core.maxVersionExclusive",
118
- `requer OonCore anterior a ${compatibility.core.maxVersionExclusive}; instalado ${coreVersion}.`,
119
- );
83
+ addIssue(issues, "compatibility.core.maxVersionExclusive", `requer OonCore anterior a ${compatibility.core.maxVersionExclusive}; instalado ${coreVersion}.`);
120
84
  }
121
85
  }
122
86
 
@@ -133,10 +97,7 @@ function validateActivation(activation, issues) {
133
97
  if (hasOwn(activation, "enabled") && typeof activation.enabled !== "boolean") {
134
98
  addIssue(issues, "activation.enabled", "deve ser booleano.");
135
99
  }
136
- if (
137
- hasOwn(activation, "configurationVersion")
138
- && (!Number.isInteger(activation.configurationVersion) || activation.configurationVersion < 1)
139
- ) {
100
+ if (hasOwn(activation, "configurationVersion") && (!Number.isInteger(activation.configurationVersion) || activation.configurationVersion < 1)) {
140
101
  addIssue(issues, "activation.configurationVersion", "deve ser inteiro maior ou igual a 1.");
141
102
  }
142
103
  if (hasOwn(activation, "fields") && !Array.isArray(activation.fields)) {
@@ -147,84 +108,50 @@ function validateActivation(activation, issues) {
147
108
  function validateAppManifest(input, { coreVersion = corePackage.version } = {}) {
148
109
  const issues = [];
149
110
  if (!isPlainObject(input)) {
150
- throw new AppManifestError("central.app.json inválido.", [
151
- { path: "$", message: "deve conter um objeto JSON." },
152
- ]);
111
+ throw new AppManifestError("central.app.json inválido.", [{ path: "$", message: "deve conter um objeto JSON." }]);
153
112
  }
154
-
155
113
  const allowedTopLevel = new Set([
156
- "$schema",
157
- "schemaVersion",
158
- "id",
159
- "name",
160
- "slug",
161
- "appKind",
162
- "modules",
163
- "capabilities",
164
- "compatibility",
165
- "activation",
114
+ "$schema", "schemaVersion", "id", "name", "slug", "appKind", "modules", "capabilities",
115
+ "compatibility", "activation", "rbac",
166
116
  ]);
167
117
  for (const key of Object.keys(input)) {
168
118
  if (!allowedTopLevel.has(key)) addIssue(issues, key, "propriedade não suportada.");
169
119
  }
170
-
171
120
  if (input.schemaVersion !== APP_MANIFEST_SCHEMA_VERSION) {
172
- addIssue(
173
- issues,
174
- "schemaVersion",
175
- `versão não suportada. Use ${APP_MANIFEST_SCHEMA_VERSION}.`,
176
- );
121
+ addIssue(issues, "schemaVersion", `versão não suportada. Use ${APP_MANIFEST_SCHEMA_VERSION}.`);
177
122
  }
178
123
  if (typeof input.id !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(input.id)) {
179
124
  addIssue(issues, "id", "deve usar letras minúsculas, números, ponto, hífen ou sublinhado.");
180
125
  }
181
- if (typeof input.name !== "string" || !input.name.trim()) {
182
- addIssue(issues, "name", "é obrigatório e deve ser texto não vazio.");
183
- }
126
+ if (typeof input.name !== "string" || !input.name.trim()) addIssue(issues, "name", "é obrigatório e deve ser texto não vazio.");
184
127
  if (typeof input.slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.slug)) {
185
128
  addIssue(issues, "slug", "deve usar slug em letras minúsculas separado por hífen.");
186
129
  }
187
130
  if (!SUPPORTED_APP_KINDS.includes(input.appKind)) {
188
131
  addIssue(issues, "appKind", `deve ser um de: ${SUPPORTED_APP_KINDS.join(", ")}.`);
189
132
  }
190
-
191
133
  if (!isPlainObject(input.modules)) {
192
134
  addIssue(issues, "modules", "deve ser um objeto.");
193
135
  } else {
194
136
  for (const key of Object.keys(input.modules)) {
195
- if (!SUPPORTED_MODULES.includes(key)) {
196
- addIssue(issues, `modules.${key}`, `módulo não suportado. Use: ${SUPPORTED_MODULES.join(", ")}.`);
197
- } else if (typeof input.modules[key] !== "boolean") {
198
- addIssue(issues, `modules.${key}`, "deve ser booleano.");
199
- }
137
+ if (!SUPPORTED_MODULES.includes(key)) addIssue(issues, `modules.${key}`, `módulo não suportado. Use: ${SUPPORTED_MODULES.join(", ")}.`);
138
+ else if (typeof input.modules[key] !== "boolean") addIssue(issues, `modules.${key}`, "deve ser booleano.");
200
139
  }
201
140
  }
202
-
203
141
  if (!Array.isArray(input.capabilities)) {
204
142
  addIssue(issues, "capabilities", "deve ser uma lista.");
205
143
  } else {
206
144
  const seen = new Set();
207
145
  input.capabilities.forEach((capability, index) => {
208
- if (
209
- typeof capability !== "string"
210
- || !/^[a-z][a-z0-9]*(?:[._:-][a-z0-9][a-z0-9-]*)+$/.test(capability)
211
- ) {
212
- addIssue(
213
- issues,
214
- `capabilities[${index}]`,
215
- "deve ser um identificador namespaced, por exemplo core.collections.",
216
- );
217
- } else if (seen.has(capability)) {
218
- addIssue(issues, `capabilities[${index}]`, `capability duplicada: ${capability}.`);
219
- } else {
220
- seen.add(capability);
221
- }
146
+ if (typeof capability !== "string" || !/^[a-z][a-z0-9]*(?:[._:-][a-z0-9][a-z0-9-]*)+$/.test(capability)) {
147
+ addIssue(issues, `capabilities[${index}]`, "deve ser um identificador namespaced, por exemplo core.collections.");
148
+ } else if (seen.has(capability)) addIssue(issues, `capabilities[${index}]`, `capability duplicada: ${capability}.`);
149
+ else seen.add(capability);
222
150
  });
223
151
  }
224
-
225
152
  validateCompatibility(input.compatibility, issues, coreVersion);
226
153
  validateActivation(input.activation, issues);
227
-
154
+ validateRbac(input.rbac, issues);
228
155
  if (issues.length) throw new AppManifestError("central.app.json inválido.", issues);
229
156
  return JSON.parse(JSON.stringify(input));
230
157
  }
@@ -233,9 +160,7 @@ function resolveAppManifestPath(cwd = process.cwd()) {
233
160
  const absolute = path.resolve(cwd);
234
161
  const candidates = [path.join(absolute, APP_MANIFEST_FILENAME)];
235
162
  const parent = path.dirname(absolute);
236
- if (["backend", "frontend"].includes(path.basename(absolute))) {
237
- candidates.push(path.join(parent, APP_MANIFEST_FILENAME));
238
- }
163
+ if (["backend", "frontend"].includes(path.basename(absolute))) candidates.push(path.join(parent, APP_MANIFEST_FILENAME));
239
164
  return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
240
165
  }
241
166
 
@@ -254,6 +179,7 @@ function appManifestToConfig(manifest, manifestPath) {
254
179
  },
255
180
  modules: { ...manifest.modules },
256
181
  capabilities: [...manifest.capabilities],
182
+ rbac: manifest.rbac ? JSON.parse(JSON.stringify(manifest.rbac)) : undefined,
257
183
  appManifest: manifest,
258
184
  appManifestPath: manifestPath,
259
185
  };
@@ -266,17 +192,13 @@ function registerAppManifest(input, options = {}) {
266
192
  }
267
193
 
268
194
  function loadAppManifest(cwdOrFile = process.cwd(), options = {}) {
269
- const manifestPath = cwdOrFile.endsWith?.(".json")
270
- ? path.resolve(cwdOrFile)
271
- : resolveAppManifestPath(cwdOrFile);
195
+ const manifestPath = cwdOrFile.endsWith?.(".json") ? path.resolve(cwdOrFile) : resolveAppManifestPath(cwdOrFile);
272
196
  let input;
273
197
  try {
274
198
  input = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
275
199
  } catch (error) {
276
200
  if (error.code === "ENOENT") throw error;
277
- throw new AppManifestError("Não foi possível ler central.app.json.", [
278
- { path: "$", message: error.message },
279
- ], { cause: error });
201
+ throw new AppManifestError("Não foi possível ler central.app.json.", [{ path: "$", message: error.message }], { cause: error });
280
202
  }
281
203
  return registerAppManifest(input, { ...options, manifestPath });
282
204
  }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ const {
4
+ CORE_PERMISSIONS,
5
+ isPermission,
6
+ isRoleCode,
7
+ } = require("../core/rbac/policy");
8
+
9
+ function isPlainObject(value) {
10
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ function validateRbac(rbac, issues) {
14
+ if (rbac === undefined) return;
15
+ if (!isPlainObject(rbac)) {
16
+ issues.push({ path: "rbac", message: "deve ser um objeto." });
17
+ return;
18
+ }
19
+
20
+ const allowedKeys = new Set(["roles", "permissions", "invitationTtlHours"]);
21
+ for (const key of Object.keys(rbac)) {
22
+ if (!allowedKeys.has(key)) issues.push({ path: `rbac.${key}`, message: "propriedade não suportada." });
23
+ }
24
+
25
+ const customPermissions = Array.isArray(rbac.permissions) ? rbac.permissions : [];
26
+ if (rbac.permissions !== undefined && !Array.isArray(rbac.permissions)) {
27
+ issues.push({ path: "rbac.permissions", message: "deve ser uma lista." });
28
+ }
29
+ const seenPermissions = new Set();
30
+ customPermissions.forEach((permission, index) => {
31
+ if (!isPermission(permission) || permission === "*") {
32
+ issues.push({ path: `rbac.permissions[${index}]`, message: "deve usar o formato recurso.acao." });
33
+ } else if (seenPermissions.has(permission)) {
34
+ issues.push({ path: `rbac.permissions[${index}]`, message: `permissão duplicada: ${permission}.` });
35
+ }
36
+ seenPermissions.add(permission);
37
+ });
38
+
39
+ if (!Array.isArray(rbac.roles) || !rbac.roles.length) {
40
+ issues.push({ path: "rbac.roles", message: "deve conter ao menos um perfil." });
41
+ return;
42
+ }
43
+
44
+ const catalog = new Set([...CORE_PERMISSIONS, ...customPermissions]);
45
+ const seenRoles = new Set();
46
+ let adminCount = 0;
47
+ rbac.roles.forEach((role, index) => {
48
+ const base = `rbac.roles[${index}]`;
49
+ if (!isPlainObject(role)) {
50
+ issues.push({ path: base, message: "deve ser um objeto." });
51
+ return;
52
+ }
53
+ const allowedRoleKeys = new Set(["code", "name", "description", "permissions", "admin"]);
54
+ for (const key of Object.keys(role)) {
55
+ if (!allowedRoleKeys.has(key)) issues.push({ path: `${base}.${key}`, message: "propriedade não suportada." });
56
+ }
57
+ if (!isRoleCode(role.code)) issues.push({ path: `${base}.code`, message: "deve ser um código estável em minúsculas." });
58
+ if (seenRoles.has(role.code)) issues.push({ path: `${base}.code`, message: `perfil duplicado: ${role.code}.` });
59
+ seenRoles.add(role.code);
60
+ if (typeof role.name !== "string" || !role.name.trim()) issues.push({ path: `${base}.name`, message: "é obrigatório." });
61
+ if (role.admin !== undefined && typeof role.admin !== "boolean") issues.push({ path: `${base}.admin`, message: "deve ser booleano." });
62
+ if (!Array.isArray(role.permissions)) {
63
+ issues.push({ path: `${base}.permissions`, message: "deve ser uma lista." });
64
+ return;
65
+ }
66
+ const seenRolePermissions = new Set();
67
+ role.permissions.forEach((permission, permissionIndex) => {
68
+ const path = `${base}.permissions[${permissionIndex}]`;
69
+ if (!isPermission(permission)) issues.push({ path, message: "permissão inválida." });
70
+ else if (permission !== "*" && !catalog.has(permission)) issues.push({ path, message: `permissão não declarada: ${permission}.` });
71
+ if (seenRolePermissions.has(permission)) issues.push({ path, message: `permissão duplicada: ${permission}.` });
72
+ seenRolePermissions.add(permission);
73
+ });
74
+ if (role.permissions.includes("*") && role.admin !== true) {
75
+ issues.push({ path: `${base}.admin`, message: "deve ser true quando o perfil usa wildcard (*)." });
76
+ }
77
+ if (role.admin === true || role.permissions.includes("*")) adminCount += 1;
78
+ });
79
+
80
+ if (!adminCount) issues.push({ path: "rbac.roles", message: "deve existir ao menos um perfil administrativo." });
81
+
82
+ if (rbac.invitationTtlHours !== undefined) {
83
+ if (!Number.isInteger(rbac.invitationTtlHours) || rbac.invitationTtlHours < 1 || rbac.invitationTtlHours > 720) {
84
+ issues.push({ path: "rbac.invitationTtlHours", message: "deve ser inteiro entre 1 e 720 horas." });
85
+ }
86
+ }
87
+ }
88
+
89
+ module.exports = { validateRbac };