@oondemand/oon-core-back 0.3.43 → 0.3.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -2,18 +2,12 @@ const express = require("express");
2
2
  const registry = require("../core/registry");
3
3
  const ctx = require("../core/context");
4
4
 
5
- /**
6
- * Rotas /core/* — contrato de metadata com o oonCore-front (Seção 2.5/21).
7
- * Expõem o que o registry conhece, sem domínio hardcoded.
8
- */
9
5
  const router = express.Router();
10
6
 
11
7
  function modelSummary(entry) {
12
8
  return {
13
9
  name: entry.name,
14
10
  singular: entry.singular,
15
- // basePath em que o CRUD está montado — consumido pelo oonCore-front para
16
- // resolver a URL do recurso no modo "dynamic" (default = /<singular>s).
17
11
  basePath: entry.definition.basePath || `/${entry.singular}s`,
18
12
  crud: entry.definition.crud?.enabled !== false,
19
13
  roles: entry.definition.crud?.roles || {},
@@ -22,9 +16,25 @@ function modelSummary(entry) {
22
16
  };
23
17
  }
24
18
 
19
+ function appSummary() {
20
+ const manifest = ctx.config.appManifest;
21
+ if (!manifest) return undefined;
22
+ return {
23
+ schemaVersion: manifest.schemaVersion,
24
+ id: manifest.id,
25
+ name: manifest.name,
26
+ slug: manifest.slug,
27
+ appKind: manifest.appKind,
28
+ modules: manifest.modules,
29
+ capabilities: manifest.capabilities,
30
+ compatibility: manifest.compatibility,
31
+ };
32
+ }
33
+
25
34
  router.get("/metadata", (_req, res) => {
26
35
  res.json({
27
36
  service: { name: ctx.config.serviceName, version: ctx.config.serviceVersion },
37
+ app: appSummary(),
28
38
  models: registry.listModels().map(modelSummary),
29
39
  collections: registry.listCollections(),
30
40
  documents: registry.listDocuments(),
@@ -35,21 +45,16 @@ router.get("/metadata", (_req, res) => {
35
45
  });
36
46
  });
37
47
 
38
- router.get("/models", (_req, res) => {
39
- res.json({ results: registry.listModels().map(modelSummary) });
40
- });
41
-
48
+ router.get("/models", (_req, res) => res.json({ results: registry.listModels().map(modelSummary) }));
42
49
  router.get("/models/:name", (req, res) => {
43
50
  const entry = registry.getModel(req.params.name);
44
51
  if (!entry) return res.status(404).json({ message: "Model não registrada." });
45
52
  res.json(modelSummary(entry));
46
53
  });
47
-
48
54
  router.get("/collections", (_req, res) => res.json({ results: registry.listCollections() }));
49
55
  router.get("/documents", (_req, res) => res.json({ results: registry.listDocuments() }));
50
56
  router.get("/pipelines", (_req, res) => res.json({ results: registry.listPipelines() }));
51
57
  router.get("/integrations", (_req, res) => res.json({ results: registry.listIntegrations() }));
52
-
53
58
  router.get("/permissions", (_req, res) => {
54
59
  const permissions = registry.listModels().flatMap((entry) => {
55
60
  const roles = entry.definition.crud?.roles || {};
@@ -60,7 +65,6 @@ router.get("/permissions", (_req, res) => {
60
65
  });
61
66
  res.json({ results: permissions });
62
67
  });
63
-
64
68
  router.get("/actions", (_req, res) => {
65
69
  const actions = registry.listModels().map((entry) => ({
66
70
  model: entry.name,
@@ -68,7 +72,6 @@ router.get("/actions", (_req, res) => {
68
72
  }));
69
73
  res.json({ results: actions });
70
74
  });
71
-
72
75
  router.get("/menus", (_req, res) => res.json({ results: registry.listMenus() }));
73
76
  router.get("/features", (_req, res) => res.json({ results: registry.listFeatures() }));
74
77
 
@@ -1,23 +1,17 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
3
  const ctx = require("../core/context");
4
+ const {
5
+ APP_MANIFEST_FILENAME,
6
+ resolveAppManifestPath,
7
+ loadAppManifest,
8
+ AppManifestError,
9
+ } = require("../manifest/appManifest");
4
10
  const {
5
11
  DOMAIN_MANIFEST_FILENAME,
6
12
  loadDomainManifest,
7
13
  } = require("../manifest/domainManifest");
8
14
 
9
- /**
10
- * Descobre e carrega a Central do consumidor:
11
- * 1. `central.config.js` na raiz do projeto (mescla na config do Core);
12
- * 2. `central.domain.json` na raiz (registra models declarativas);
13
- * 3. arquivos de domínio em `src/{models,validations,triggers,routes,
14
- * pipelines,documents,integrations,mappings}` (apenas `require`,
15
- * o efeito colateral é registrar via `define*`).
16
- *
17
- * O carregamento dos diretórios JavaScript permanece ativo durante a migração.
18
- * Declarar a mesma model no manifesto e em `src/models` falha explicitamente no
19
- * registry, impedindo duas fontes de verdade silenciosas.
20
- */
21
15
  const DOMAIN_DIRS = [
22
16
  "models",
23
17
  "validations",
@@ -30,11 +24,51 @@ const DOMAIN_DIRS = [
30
24
  "hooks",
31
25
  ];
32
26
 
27
+ const APP_CONFIG_KEYS = new Set([
28
+ "name",
29
+ "slug",
30
+ "appCode",
31
+ "appCodigo",
32
+ "appKind",
33
+ "ecosystem",
34
+ "activation",
35
+ "modules",
36
+ "capabilities",
37
+ ]);
38
+
39
+ function assertConfigBoundary(config, appManifest) {
40
+ if (!appManifest || !config || typeof config !== "object") return;
41
+ const issues = [];
42
+ for (const key of APP_CONFIG_KEYS) {
43
+ if (Object.prototype.hasOwnProperty.call(config, key)) {
44
+ issues.push({
45
+ path: `central.config.js.${key}`,
46
+ message: `deve ser declarado em ${APP_MANIFEST_FILENAME}.`,
47
+ });
48
+ }
49
+ }
50
+ if (appManifest.appKind !== "root-central" && Object.prototype.hasOwnProperty.call(config, "auth")) {
51
+ issues.push({
52
+ path: "central.config.js.auth",
53
+ message: "autenticação de Centrais member/portal é fornecida pelo OonCore.",
54
+ });
55
+ }
56
+ if (issues.length) throw new AppManifestError("central.config.js viola a fronteira declarativa.", issues);
57
+ }
58
+
33
59
  function loadCentral(cwd = process.cwd()) {
60
+ let appManifest;
61
+ const appManifestPath = resolveAppManifestPath(cwd);
62
+ if (fs.existsSync(appManifestPath)) {
63
+ appManifest = loadAppManifest(appManifestPath);
64
+ }
65
+
34
66
  const configPath = path.join(cwd, "central.config.js");
35
67
  if (fs.existsSync(configPath)) {
36
- const config = require(configPath);
37
- ctx.setConfig(typeof config === "function" ? config() : config);
68
+ const exported = require(configPath);
69
+ const config = typeof exported === "function" ? exported() : exported;
70
+ assertConfigBoundary(config, appManifest);
71
+ ctx.setConfig(config);
38
72
  }
39
73
 
40
74
  const domainManifestPath = path.join(cwd, DOMAIN_MANIFEST_FILENAME);
@@ -54,12 +88,9 @@ function loadCentral(cwd = process.cwd()) {
54
88
  function requireDir(dir) {
55
89
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
56
90
  const full = path.join(dir, entry.name);
57
- if (entry.isDirectory()) {
58
- requireDir(full);
59
- } else if (/\.(js|cjs)$/.test(entry.name)) {
60
- require(full);
61
- }
91
+ if (entry.isDirectory()) requireDir(full);
92
+ else if (/\.(js|cjs)$/.test(entry.name)) require(full);
62
93
  }
63
94
  }
64
95
 
65
- module.exports = { loadCentral, DOMAIN_DIRS };
96
+ module.exports = { loadCentral, DOMAIN_DIRS, APP_CONFIG_KEYS, assertConfigBoundary };
@@ -8,11 +8,6 @@ const {
8
8
  getTimeout,
9
9
  } = require("../utils/activationProvider");
10
10
 
11
- /**
12
- * Verificador padrão: delega para a Central de Ativações e valida o token no
13
- * contexto do app configurado no backend por APP_CODE/APP_CODIGO.
14
- * Pode ser sobrescrito via `central.config.js -> auth.verifyToken`.
15
- */
16
11
  async function defaultVerifyToken(token, { req } = {}) {
17
12
  const baseUrl = getBaseUrl(req);
18
13
  const appCode = getAppCode();
@@ -29,9 +24,7 @@ async function defaultVerifyToken(token, { req } = {}) {
29
24
  });
30
25
  } catch (error) {
31
26
  const err = new Error(
32
- error.response?.data?.message ||
33
- error.message ||
34
- "Token inválido ou erro na autenticação.",
27
+ error.response?.data?.message || error.message || "Token inválido ou erro na autenticação.",
35
28
  );
36
29
  err.statusCode = error.response?.status || error.statusCode || 401;
37
30
  throw err;
@@ -44,9 +37,7 @@ async function defaultVerifyToken(token, { req } = {}) {
44
37
  throw err;
45
38
  }
46
39
 
47
- const tipoAcesso =
48
- data.aplicativo?.tipoAcesso || data.tipoAcesso || data.tipo || data.perfil;
49
-
40
+ const tipoAcesso = data.aplicativo?.tipoAcesso || data.tipoAcesso || data.tipo || data.perfil;
50
41
  return {
51
42
  ...data,
52
43
  tipo: tipoAcesso === "master" ? "admin" : tipoAcesso,
@@ -56,9 +47,32 @@ async function defaultVerifyToken(token, { req } = {}) {
56
47
  };
57
48
  }
58
49
 
50
+ function localDevelopmentVerifyToken(token) {
51
+ const expected = String(process.env.DEV_TOKEN || "");
52
+ if (process.env.NODE_ENV !== "development" || !expected || token !== expected) {
53
+ const error = new Error("Token local de desenvolvimento inválido.");
54
+ error.statusCode = 401;
55
+ throw error;
56
+ }
57
+ return {
58
+ tipo: process.env.DEV_USER_ROLE || "admin",
59
+ perfil: process.env.DEV_USER_ROLE || "admin",
60
+ nome: process.env.DEV_USER_NAME || "Dev Local",
61
+ email: process.env.DEV_USER_EMAIL || "dev@local",
62
+ capabilities: ctx.config.capabilities || [],
63
+ };
64
+ }
65
+
66
+ function resolveVerifyToken() {
67
+ if (typeof ctx.config.auth?.verifyToken === "function") return ctx.config.auth.verifyToken;
68
+ if (process.env.NODE_ENV === "development" && process.env.DEV_TOKEN) {
69
+ return localDevelopmentVerifyToken;
70
+ }
71
+ return defaultVerifyToken;
72
+ }
73
+
59
74
  const authMiddleware = async (req, res, next) => {
60
75
  const token = req.headers.authorization?.split(" ")[1];
61
-
62
76
  if (!token) {
63
77
  return Helpers.sendErrorResponse({
64
78
  res,
@@ -67,10 +81,8 @@ const authMiddleware = async (req, res, next) => {
67
81
  });
68
82
  }
69
83
 
70
- const verify = ctx.config.auth?.verifyToken || defaultVerifyToken;
71
-
72
84
  try {
73
- const usuario = await verify(token, { req });
85
+ const usuario = await resolveVerifyToken()(token, { req });
74
86
  if (!usuario) {
75
87
  const error = new Error("Usuário não resolvido.");
76
88
  error.statusCode = 401;
@@ -89,3 +101,5 @@ const authMiddleware = async (req, res, next) => {
89
101
 
90
102
  module.exports = authMiddleware;
91
103
  module.exports.defaultVerifyToken = defaultVerifyToken;
104
+ module.exports.localDevelopmentVerifyToken = localDevelopmentVerifyToken;
105
+ module.exports.resolveVerifyToken = resolveVerifyToken;
package/src/index.js CHANGED
@@ -1,12 +1,5 @@
1
1
  require("dotenv").config();
2
2
 
3
- /**
4
- * API pública do @oondemand/oon-core-back (Seção 2.4).
5
- *
6
- * O consumidor escreve apenas models/validations/triggers/hooks/mappings/
7
- * pipelines/documents/integrations/routes e sobe com `start()` ou
8
- * `oonCore-back start`.
9
- */
10
3
  const { start } = require("./boot/startServer");
11
4
  const { createApp } = require("./boot/createApp");
12
5
  const { activate } = require("./boot/activation");
@@ -15,16 +8,14 @@ const fields = require("./core/fields");
15
8
  const registry = require("./core/registry");
16
9
  const GenericError = require("./core/errors/GenericError");
17
10
  const { omie } = require("./modules/omie");
11
+ const appManifest = require("./manifest/appManifest");
18
12
  const domainManifest = require("./manifest/domainManifest");
19
13
  const domainRules = require("./manifest/domainRules");
20
14
 
21
15
  module.exports = {
22
- // boot
23
16
  start,
24
17
  createApp,
25
18
  activate,
26
-
27
- // declaração de domínio
28
19
  defineCentral: define.defineCentral,
29
20
  defineModel: define.defineModel,
30
21
  defineCollection: define.defineCollection,
@@ -35,7 +26,17 @@ module.exports = {
35
26
  defineValidation: define.defineValidation,
36
27
  defineTrigger: define.defineTrigger,
37
28
 
38
- // manifesto declarativo de domínio
29
+ APP_MANIFEST_FILENAME: appManifest.APP_MANIFEST_FILENAME,
30
+ APP_MANIFEST_SCHEMA_VERSION: appManifest.APP_MANIFEST_SCHEMA_VERSION,
31
+ SUPPORTED_APP_KINDS: appManifest.SUPPORTED_APP_KINDS,
32
+ SUPPORTED_APP_MODULES: appManifest.SUPPORTED_MODULES,
33
+ AppManifestError: appManifest.AppManifestError,
34
+ validateAppManifest: appManifest.validateAppManifest,
35
+ resolveAppManifestPath: appManifest.resolveAppManifestPath,
36
+ appManifestToConfig: appManifest.appManifestToConfig,
37
+ registerAppManifest: appManifest.registerAppManifest,
38
+ loadAppManifest: appManifest.loadAppManifest,
39
+
39
40
  DOMAIN_MANIFEST_FILENAME: domainManifest.DOMAIN_MANIFEST_FILENAME,
40
41
  DOMAIN_MANIFEST_SCHEMA_VERSION: domainManifest.DOMAIN_MANIFEST_SCHEMA_VERSION,
41
42
  SUPPORTED_DOMAIN_FIELD_KINDS: domainManifest.SUPPORTED_FIELD_KINDS,
@@ -49,11 +50,8 @@ module.exports = {
49
50
  evaluateDomainExpression: domainRules.evaluateDomainExpression,
50
51
  applyDomainMutation: domainRules.applyDomainMutation,
51
52
 
52
- // helpers
53
53
  fields,
54
54
  omie,
55
55
  GenericError,
56
-
57
- // avançado / introspecção
58
56
  registry,
59
57
  };
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const ctx = require("../core/context");
6
+ const corePackage = require("../../package.json");
7
+
8
+ const APP_MANIFEST_FILENAME = "central.app.json";
9
+ const APP_MANIFEST_SCHEMA_VERSION = 1;
10
+ const SUPPORTED_APP_KINDS = Object.freeze([
11
+ "root-central",
12
+ "member-central",
13
+ "portal-cockpit",
14
+ ]);
15
+ const SUPPORTED_MODULES = Object.freeze([
16
+ "collections",
17
+ "documents",
18
+ "pipelines",
19
+ "integrations",
20
+ "omie",
21
+ "assistants",
22
+ "currencies",
23
+ ]);
24
+ const APP_KIND_ROLE = Object.freeze({
25
+ "root-central": "root",
26
+ "member-central": "member",
27
+ "portal-cockpit": "portal",
28
+ });
29
+
30
+ class AppManifestError extends Error {
31
+ constructor(message, issues = [], options = {}) {
32
+ super(message, options);
33
+ this.name = "AppManifestError";
34
+ this.code = "OON_APP_MANIFEST_INVALID";
35
+ this.statusCode = 422;
36
+ this.issues = issues;
37
+ this.details = { issues };
38
+ }
39
+ }
40
+
41
+ function isPlainObject(value) {
42
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
43
+ }
44
+
45
+ function hasOwn(object, key) {
46
+ return Object.prototype.hasOwnProperty.call(object, key);
47
+ }
48
+
49
+ function addIssue(issues, fieldPath, message) {
50
+ issues.push({ path: fieldPath, message });
51
+ }
52
+
53
+ function parseVersion(value) {
54
+ if (typeof value !== "string") return null;
55
+ 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);
58
+ }
59
+
60
+ function compareVersions(left, right) {
61
+ for (let index = 0; index < 3; index += 1) {
62
+ if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
63
+ }
64
+ return 0;
65
+ }
66
+
67
+ function validateCompatibility(compatibility, issues, coreVersion) {
68
+ if (!isPlainObject(compatibility)) {
69
+ addIssue(issues, "compatibility", "deve ser um objeto.");
70
+ return;
71
+ }
72
+ if (!isPlainObject(compatibility.core)) {
73
+ addIssue(issues, "compatibility.core", "deve ser um objeto.");
74
+ return;
75
+ }
76
+
77
+ const allowedKeys = new Set(["minVersion", "maxVersionExclusive"]);
78
+ for (const key of Object.keys(compatibility.core)) {
79
+ if (!allowedKeys.has(key)) addIssue(issues, `compatibility.core.${key}`, "propriedade não suportada.");
80
+ }
81
+
82
+ const minVersion = parseVersion(compatibility.core.minVersion);
83
+ const maxVersion = hasOwn(compatibility.core, "maxVersionExclusive")
84
+ ? parseVersion(compatibility.core.maxVersionExclusive)
85
+ : null;
86
+
87
+ if (!minVersion) {
88
+ addIssue(issues, "compatibility.core.minVersion", "deve usar versão semântica, por exemplo 0.3.45.");
89
+ }
90
+ 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
+ );
96
+ }
97
+ if (minVersion && maxVersion && compareVersions(minVersion, maxVersion) >= 0) {
98
+ addIssue(
99
+ issues,
100
+ "compatibility.core",
101
+ "minVersion deve ser menor que maxVersionExclusive.",
102
+ );
103
+ }
104
+
105
+ const installedVersion = parseVersion(coreVersion);
106
+ if (!installedVersion || !minVersion) return;
107
+ if (compareVersions(installedVersion, minVersion) < 0) {
108
+ addIssue(
109
+ issues,
110
+ "compatibility.core.minVersion",
111
+ `requer OonCore ${compatibility.core.minVersion} ou superior; instalado ${coreVersion}.`,
112
+ );
113
+ }
114
+ 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
+ );
120
+ }
121
+ }
122
+
123
+ function validateActivation(activation, issues) {
124
+ if (!hasOwn(activation || {}, "enabled") && activation === undefined) return;
125
+ if (!isPlainObject(activation)) {
126
+ addIssue(issues, "activation", "deve ser um objeto.");
127
+ return;
128
+ }
129
+ const allowedKeys = new Set(["enabled", "configurationVersion", "fields"]);
130
+ for (const key of Object.keys(activation)) {
131
+ if (!allowedKeys.has(key)) addIssue(issues, `activation.${key}`, "propriedade não suportada.");
132
+ }
133
+ if (hasOwn(activation, "enabled") && typeof activation.enabled !== "boolean") {
134
+ addIssue(issues, "activation.enabled", "deve ser booleano.");
135
+ }
136
+ if (
137
+ hasOwn(activation, "configurationVersion")
138
+ && (!Number.isInteger(activation.configurationVersion) || activation.configurationVersion < 1)
139
+ ) {
140
+ addIssue(issues, "activation.configurationVersion", "deve ser inteiro maior ou igual a 1.");
141
+ }
142
+ if (hasOwn(activation, "fields") && !Array.isArray(activation.fields)) {
143
+ addIssue(issues, "activation.fields", "deve ser uma lista.");
144
+ }
145
+ }
146
+
147
+ function validateAppManifest(input, { coreVersion = corePackage.version } = {}) {
148
+ const issues = [];
149
+ if (!isPlainObject(input)) {
150
+ throw new AppManifestError("central.app.json inválido.", [
151
+ { path: "$", message: "deve conter um objeto JSON." },
152
+ ]);
153
+ }
154
+
155
+ const allowedTopLevel = new Set([
156
+ "$schema",
157
+ "schemaVersion",
158
+ "id",
159
+ "name",
160
+ "slug",
161
+ "appKind",
162
+ "modules",
163
+ "capabilities",
164
+ "compatibility",
165
+ "activation",
166
+ ]);
167
+ for (const key of Object.keys(input)) {
168
+ if (!allowedTopLevel.has(key)) addIssue(issues, key, "propriedade não suportada.");
169
+ }
170
+
171
+ 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
+ );
177
+ }
178
+ if (typeof input.id !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(input.id)) {
179
+ addIssue(issues, "id", "deve usar letras minúsculas, números, ponto, hífen ou sublinhado.");
180
+ }
181
+ if (typeof input.name !== "string" || !input.name.trim()) {
182
+ addIssue(issues, "name", "é obrigatório e deve ser texto não vazio.");
183
+ }
184
+ if (typeof input.slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.slug)) {
185
+ addIssue(issues, "slug", "deve usar slug em letras minúsculas separado por hífen.");
186
+ }
187
+ if (!SUPPORTED_APP_KINDS.includes(input.appKind)) {
188
+ addIssue(issues, "appKind", `deve ser um de: ${SUPPORTED_APP_KINDS.join(", ")}.`);
189
+ }
190
+
191
+ if (!isPlainObject(input.modules)) {
192
+ addIssue(issues, "modules", "deve ser um objeto.");
193
+ } else {
194
+ 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
+ }
200
+ }
201
+ }
202
+
203
+ if (!Array.isArray(input.capabilities)) {
204
+ addIssue(issues, "capabilities", "deve ser uma lista.");
205
+ } else {
206
+ const seen = new Set();
207
+ 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
+ }
222
+ });
223
+ }
224
+
225
+ validateCompatibility(input.compatibility, issues, coreVersion);
226
+ validateActivation(input.activation, issues);
227
+
228
+ if (issues.length) throw new AppManifestError("central.app.json inválido.", issues);
229
+ return JSON.parse(JSON.stringify(input));
230
+ }
231
+
232
+ function resolveAppManifestPath(cwd = process.cwd()) {
233
+ const absolute = path.resolve(cwd);
234
+ const candidates = [path.join(absolute, APP_MANIFEST_FILENAME)];
235
+ const parent = path.dirname(absolute);
236
+ if (["backend", "frontend"].includes(path.basename(absolute))) {
237
+ candidates.push(path.join(parent, APP_MANIFEST_FILENAME));
238
+ }
239
+ return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
240
+ }
241
+
242
+ function appManifestToConfig(manifest, manifestPath) {
243
+ return {
244
+ serviceName: manifest.slug,
245
+ name: manifest.name,
246
+ slug: manifest.slug,
247
+ appCode: manifest.id,
248
+ appKind: manifest.appKind,
249
+ ecosystem: { role: APP_KIND_ROLE[manifest.appKind] },
250
+ activation: manifest.activation || {
251
+ enabled: manifest.appKind !== "portal-cockpit",
252
+ configurationVersion: 1,
253
+ fields: [],
254
+ },
255
+ modules: { ...manifest.modules },
256
+ capabilities: [...manifest.capabilities],
257
+ appManifest: manifest,
258
+ appManifestPath: manifestPath,
259
+ };
260
+ }
261
+
262
+ function registerAppManifest(input, options = {}) {
263
+ const manifest = validateAppManifest(input, options);
264
+ ctx.setConfig(appManifestToConfig(manifest, options.manifestPath));
265
+ return manifest;
266
+ }
267
+
268
+ function loadAppManifest(cwdOrFile = process.cwd(), options = {}) {
269
+ const manifestPath = cwdOrFile.endsWith?.(".json")
270
+ ? path.resolve(cwdOrFile)
271
+ : resolveAppManifestPath(cwdOrFile);
272
+ let input;
273
+ try {
274
+ input = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
275
+ } catch (error) {
276
+ 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 });
280
+ }
281
+ return registerAppManifest(input, { ...options, manifestPath });
282
+ }
283
+
284
+ module.exports = {
285
+ APP_MANIFEST_FILENAME,
286
+ APP_MANIFEST_SCHEMA_VERSION,
287
+ SUPPORTED_APP_KINDS,
288
+ SUPPORTED_MODULES,
289
+ AppManifestError,
290
+ parseVersion,
291
+ compareVersions,
292
+ validateAppManifest,
293
+ resolveAppManifestPath,
294
+ appManifestToConfig,
295
+ registerAppManifest,
296
+ loadAppManifest,
297
+ };