@oondemand/oon-core-back 0.2.0 → 0.3.0

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/README.md CHANGED
@@ -1,132 +1,19 @@
1
- # @oondemand/oon-core-back
1
+ # OonCore Back - ativação de instâncias
2
2
 
3
- 📖 **[Documentação completa → ooncoredoc.vercel.app](https://ooncoredoc.vercel.app)**
3
+ O Core suporta `ecosystem.role` em `central.config.js`: `root` para a Central de Ativações e `member` (padrão) para demais Centrais. Aplicações `member` iniciam como `nao_ativada`, expõem apenas `/ativacao/*`, `/health` e `/version`, e só liberam autenticação/CRUD após ativação.
4
4
 
5
- Core runtime para **Centrais Oon**. O backend de uma Central passa a ser
6
- apenas domínio: você escreve `central.config.js` + `src/models` (e, quando
7
- preciso, `validations / triggers / pipelines / documents / integrations /
8
- routes`) e sobe tudo com `oonCore-back start`.
5
+ ## Variáveis de ambiente
9
6
 
10
- Toda a infraestrutura — boot, banco, auth, RBAC, logs, auditoria, paginação,
11
- metadata, CRUD, módulos opinativos (Omie, Assistentes, Multi-moedas, Esteiras,
12
- Documentos) e deploy (Docker + k8s) — vive aqui dentro.
7
+ - `CENTRAL_ATIVACAO_URL`: backend real da Central de Ativações; nunca aponte para o próprio Core.
8
+ - `APP_CODE`: código do aplicativo no Ecossistema.
9
+ - `APP_ENVIRONMENT`: `desenvolvimento`, `homologacao` ou `producao`.
10
+ - `PUBLIC_APP_URL`: URL pública confirmada da Central.
11
+ - `INSTANCE_CREDENTIAL_ENCRYPTION_KEY`: chave para AES-256-GCM; obrigatória em produção.
12
+ - `AUTH_PROVIDER_TIMEOUT_MS`: timeout das chamadas à Central de Ativações.
13
+ - `INSTANCE_HEARTBEAT_INTERVAL_MS`: intervalo planejado para sincronização/heartbeat.
13
14
 
14
- ## Instalação
15
+ ## Fluxo
15
16
 
16
- ```bash
17
- npm install @oondemand/oon-core-back
18
- ```
17
+ `GET /ativacao/status` informa estado sem segredos. `POST /ativacao/validar-codigo` valida sem consumir. `POST /ativacao/concluir` revalida, chama `/ativar`, criptografa imediatamente o token da instância, executa hooks declarativos, chama `/concluir` e marca a instância como `ativa`. `POST /ativacao/tentar-novamente` retoma uma ativação já registrada sem exigir novo código.
19
18
 
20
- ## Uso mínimo
21
-
22
- `central.config.js`:
23
-
24
- ```js
25
- module.exports = {
26
- serviceName: "minha-central",
27
- // opcional: provedor de auth (default = Meus Apps)
28
- // auth: { verifyToken: async (token) => ({ tipo, nome, email }) },
29
- };
30
- ```
31
-
32
- `src/models/Produto.js`:
33
-
34
- ```js
35
- const { defineModel, fields } = require("@oondemand/oon-core-back");
36
-
37
- defineModel({
38
- name: "Produto",
39
- schema: {
40
- nome: fields.string({ required: true }),
41
- preco: fields.currency(),
42
- moeda: fields.currencyCode(),
43
- },
44
- crud: { enabled: true, roles: { write: ["admin"] } },
45
- });
46
- ```
47
-
48
- Subir:
49
-
50
- ```bash
51
- npx oonCore-back start # produção
52
- npx oonCore-back dev # watch/reload
53
- ```
54
-
55
- Isso já expõe, para `Produto`:
56
-
57
- ```
58
- GET /produtos (list + paginação/filtros/busca/ordenação)
59
- GET /produtos/:id
60
- POST /produtos
61
- PUT /produtos/:id
62
- PATCH /produtos/:id
63
- DELETE /produtos/:id
64
- POST /produtos/import
65
- GET /produtos/export
66
- ```
67
-
68
- Todas privadas (auth obrigatório), com RBAC e auditoria nas mutações.
69
-
70
- ## API pública
71
-
72
- ```js
73
- const {
74
- start, createApp, activate,
75
- defineCentral, defineModel, fields,
76
- defineCollection, defineDocument, definePipeline,
77
- defineOmieMapping, defineRoutes, defineValidation, defineTrigger,
78
- omie, GenericError, registry,
79
- } = require("@oondemand/oon-core-back");
80
- ```
81
-
82
- ### `defineRoutes` — rotas customizadas com o contrato de segurança
83
-
84
- Nunca se usa `express.Router()` cru. `router.private.*` aplica auth + RBAC
85
- (+ auditoria opcional); `router.public.*` é aberto.
86
-
87
- ```js
88
- const { defineRoutes } = require("@oondemand/oon-core-back");
89
-
90
- defineRoutes("/relatorios", (router) => {
91
- router.private.get("/resumo", { roles: ["admin"] }, async (req, res) => {
92
- res.json({ ok: true, usuario: req.usuario });
93
- });
94
- router.public.get("/health", async (_req, res) => res.json({ up: true }));
95
- });
96
- ```
97
-
98
- ## Rotas `/core/*` (contrato com o oonCore-front)
99
-
100
- ```
101
- GET /core/metadata GET /core/models GET /core/models/:name
102
- GET /core/collections GET /core/documents GET /core/pipelines
103
- GET /core/integrations GET /core/permissions GET /core/actions
104
- GET /core/menus GET /core/features
105
- ```
106
-
107
- ## CLI
108
-
109
- ```bash
110
- oonCore-back start # produção
111
- oonCore-back dev # watch/reload
112
- oonCore-back activate # seed/ativação (singleton Sistema + hook)
113
- oonCore-back build:image # renderiza Dockerfile + docker build
114
- oonCore-back k8s:render # renderiza manifests a partir de central.manifest.json
115
- oonCore-back deploy # kubectl apply dos manifests
116
- ```
117
-
118
- O consumidor não escreve YAML: ajusta `central.manifest.json` e o Core
119
- renderiza Dockerfile + ConfigMap/Secret/Deployment/Service/Ingress/HPA/Certificate.
120
-
121
- ## Testes
122
-
123
- ```bash
124
- npm test # invariantes de segurança (offline)
125
- MONGO_URI=... npm test # inclui CRUD + auditoria (requer Mongo)
126
- ```
127
-
128
- ## Variáveis de ambiente principais
129
-
130
- `SERVICE_NAME`, `SERVICE_VERSION`, `PORT`, `NODE_ENV`,
131
- `DB_SERVER`/`DB_NAME`/`DB_USER`/`DB_PASSWORD`/`DB_AUTH_SOURCE`/`DB_REPLICA_SET`/`DB_TSL`
132
- (ou `MONGO_URI`), `MEUS_APPS_BACKEND_URL`, `OMIE_APP_KEY`/`OMIE_APP_SECRET`.
19
+ Campos adicionais podem ser declarados em `activation.fields`; `password` e `secret` são sanitizados na configuração retornada ao frontend. Hooks opcionais: `validate`, `beforeComplete`, `afterComplete`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -0,0 +1,522 @@
1
+ const express = require("express");
2
+ const ctx = require("../core/context");
3
+ const GenericError = require("../core/errors/GenericError");
4
+ const { asyncHandler, sendResponse } = require("../core/utils/helpers");
5
+ const {
6
+ PATHS,
7
+ globalDefaults,
8
+ globalFields,
9
+ rootFields,
10
+ role,
11
+ ambiente,
12
+ publicUrl,
13
+ sanitizeFields,
14
+ activationConfig,
15
+ ensureInstance,
16
+ hasActiveRootInstance,
17
+ claimRootBootstrap,
18
+ validateRootCredentials,
19
+ encrypt,
20
+ instanceHeaders,
21
+ validateParams,
22
+ providerPost,
23
+ providerPayload,
24
+ event,
25
+ } = require("../core/utils/instanceActivation");
26
+ const { getAppCode, getBaseUrl } = require("../core/utils/activationProvider");
27
+
28
+ const router = express.Router();
29
+ const attempts = new Map();
30
+
31
+ function rateLimit(req, res, next) {
32
+ const key = req.ip || "local";
33
+ const now = Date.now();
34
+ const list = (attempts.get(key) || []).filter((time) => now - time < 60000);
35
+
36
+ if (list.length >= 30) {
37
+ return res.status(429).json({
38
+ error: {
39
+ code: "RATE_LIMIT",
40
+ message: "Muitas tentativas de ativação.",
41
+ },
42
+ });
43
+ }
44
+
45
+ list.push(now);
46
+ attempts.set(key, list);
47
+ next();
48
+ }
49
+
50
+ function assertMember() {
51
+ if (role() === "root") {
52
+ throw new GenericError(
53
+ "Aplicações root usam o bootstrap inicial do ecossistema.",
54
+ { statusCode: 400 },
55
+ );
56
+ }
57
+ }
58
+
59
+ function assertRoot() {
60
+ if (role() !== "root") {
61
+ throw new GenericError(
62
+ "Somente a aplicação root pode executar esta ativação.",
63
+ { statusCode: 400 },
64
+ );
65
+ }
66
+ }
67
+
68
+ function assertApp(data) {
69
+ const returned = data?.app?.codigo || data?.appCode;
70
+ if (returned && String(returned).toLowerCase() !== getAppCode()) {
71
+ throw new GenericError(
72
+ "Código do aplicativo retornado pela Central é incompatível.",
73
+ { statusCode: 502 },
74
+ );
75
+ }
76
+ }
77
+
78
+ function safeContext(data) {
79
+ const { credencial, ...safe } = data || {};
80
+ return safe;
81
+ }
82
+
83
+ function requireRootBootstrapResult(result) {
84
+ const usuarioId = result?.usuario?.id || result?.usuario?._id;
85
+ const tenantId = result?.tenant?.id || result?.tenant?._id;
86
+ const appId = result?.app?.id || result?.app?._id;
87
+ const licencaId = result?.licenca?.id || result?.licenca?._id;
88
+
89
+ if (!usuarioId || !tenantId || !appId || !licencaId) {
90
+ throw new GenericError(
91
+ "O bootstrap root não retornou a estrutura mínima do ecossistema.",
92
+ { statusCode: 500 },
93
+ );
94
+ }
95
+
96
+ if (
97
+ result?.app?.ecosystemRole &&
98
+ result.app.ecosystemRole !== "root"
99
+ ) {
100
+ throw new GenericError(
101
+ "O aplicativo criado pelo bootstrap não foi identificado como root.",
102
+ { statusCode: 500 },
103
+ );
104
+ }
105
+
106
+ return {
107
+ usuarioId: String(usuarioId),
108
+ tenantId: String(tenantId),
109
+ appId: String(appId),
110
+ licencaId: String(licencaId),
111
+ };
112
+ }
113
+
114
+ router.use(rateLimit);
115
+
116
+ router.get(
117
+ "/status",
118
+ asyncHandler(async (_req, res) => {
119
+ const instance = await ensureInstance();
120
+ const rootActive =
121
+ instance.ecosystemRole === "root"
122
+ ? await hasActiveRootInstance()
123
+ : false;
124
+
125
+ const canActivateRoot =
126
+ instance.ecosystemRole === "root" &&
127
+ !rootActive &&
128
+ ["nao_ativada", "erro"].includes(instance.status);
129
+
130
+ const canActivateMember =
131
+ instance.ecosystemRole === "member" &&
132
+ ["nao_ativada", "erro"].includes(instance.status) &&
133
+ !instance.instanceId;
134
+
135
+ sendResponse({
136
+ res,
137
+ role: instance.ecosystemRole,
138
+ status: instance.status,
139
+ appCode: getAppCode(),
140
+ ambiente: instance.ambiente || ambiente(),
141
+ podeAtivar: canActivateRoot || canActivateMember,
142
+ podeTentarNovamente:
143
+ instance.ecosystemRole === "member" &&
144
+ Boolean(instance.instanceId) &&
145
+ ["configurando", "erro"].includes(instance.status),
146
+ motivoSuspensao: instance.motivoSuspensao,
147
+ motivoRevogacao: instance.motivoRevogacao,
148
+ });
149
+ }),
150
+ );
151
+
152
+ router.get(
153
+ "/configuracao",
154
+ asyncHandler(async (req, res) => {
155
+ const instance = await ensureInstance();
156
+ const config = activationConfig();
157
+
158
+ if (instance.ecosystemRole === "root") {
159
+ return sendResponse({
160
+ res,
161
+ mode: "root-bootstrap",
162
+ configurationVersion: config.configurationVersion,
163
+ rootFields: sanitizeFields(rootFields),
164
+ globalFields: [],
165
+ appFields: [],
166
+ defaults: {},
167
+ technical: {
168
+ appCode: getAppCode(),
169
+ ambiente: ambiente(),
170
+ ecosystemRole: "root",
171
+ },
172
+ });
173
+ }
174
+
175
+ const host = new URL(getBaseUrl(req)).hostname;
176
+ return sendResponse({
177
+ res,
178
+ mode: "member-activation",
179
+ configurationVersion: config.configurationVersion,
180
+ rootFields: [],
181
+ globalFields,
182
+ appFields: sanitizeFields(config.fields),
183
+ defaults: {
184
+ ...globalDefaults,
185
+ ambiente: ambiente(),
186
+ urlPublica: publicUrl(req),
187
+ },
188
+ technical: {
189
+ appCode: getAppCode(),
190
+ ambiente: ambiente(),
191
+ ecosystemRole: "member",
192
+ centralAtivacaoHost: host,
193
+ },
194
+ });
195
+ }),
196
+ );
197
+
198
+ router.post(
199
+ "/ativar-root",
200
+ asyncHandler(async (req, res) => {
201
+ assertRoot();
202
+
203
+ const config = activationConfig();
204
+ if (typeof config.rootBootstrap !== "function") {
205
+ throw new GenericError(
206
+ "A aplicação root não configurou o bootstrap inicial.",
207
+ { statusCode: 500 },
208
+ );
209
+ }
210
+
211
+ const { email, password } = validateRootCredentials(
212
+ req.body?.email,
213
+ req.body?.senha,
214
+ );
215
+ const instance = await claimRootBootstrap();
216
+
217
+ event("ROOT_BOOTSTRAP_STARTED", {
218
+ correlationId: req.correlationId,
219
+ appCode: getAppCode(),
220
+ });
221
+
222
+ try {
223
+ const result = await config.rootBootstrap({
224
+ email,
225
+ password,
226
+ appCode: getAppCode(),
227
+ appName: ctx.config.name,
228
+ appSlug: ctx.config.slug,
229
+ environment: ambiente(),
230
+ context: { req },
231
+ });
232
+
233
+ const ids = requireRootBootstrapResult(result);
234
+ const now = new Date();
235
+
236
+ Object.assign(instance, {
237
+ ecosystemRole: "root",
238
+ status: "ativa",
239
+ instanceId: result.instanceId || `root:${ids.appId}`,
240
+ instanceCode: result.instanceCode || "root",
241
+ appCode: getAppCode(),
242
+ appId: ids.appId,
243
+ tenantId: ids.tenantId,
244
+ licencaId: ids.licencaId,
245
+ ambiente: ambiente(),
246
+ nomeInstancia: ctx.config.name || ctx.config.slug || getAppCode(),
247
+ publicUrl: publicUrl(req),
248
+ tenant: result.tenant,
249
+ app: result.app,
250
+ licenca: result.licenca,
251
+ parametrosGlobais: {},
252
+ parametrosAplicacao: {},
253
+ configurationVersion: config.configurationVersion,
254
+ ativadaEm: now,
255
+ concluidaEm: now,
256
+ ultimoErro: undefined,
257
+ });
258
+
259
+ await instance.save();
260
+
261
+ event("ROOT_BOOTSTRAP_COMPLETED", {
262
+ correlationId: req.correlationId,
263
+ appCode: getAppCode(),
264
+ usuarioId: ids.usuarioId,
265
+ });
266
+
267
+ return sendResponse({
268
+ res,
269
+ sucesso: true,
270
+ status: "ativa",
271
+ usuario: {
272
+ id: ids.usuarioId,
273
+ email: result.usuario.email,
274
+ },
275
+ });
276
+ } catch (error) {
277
+ instance.status = "erro";
278
+ instance.ultimoErro = error.message;
279
+ await instance.save();
280
+
281
+ event("ROOT_BOOTSTRAP_FAILED", {
282
+ correlationId: req.correlationId,
283
+ appCode: getAppCode(),
284
+ });
285
+
286
+ throw error;
287
+ }
288
+ }),
289
+ );
290
+
291
+ router.post(
292
+ "/validar-codigo",
293
+ asyncHandler(async (req, res) => {
294
+ assertMember();
295
+ const codigoAtivacao = String(req.body?.codigoAtivacao || "").trim();
296
+ if (!codigoAtivacao) {
297
+ throw new GenericError("Código de ativação obrigatório.", {
298
+ statusCode: 400,
299
+ });
300
+ }
301
+
302
+ const instance = await ensureInstance();
303
+ if (instance.status === "ativa") {
304
+ throw new GenericError("Instância já ativada.", { statusCode: 409 });
305
+ }
306
+
307
+ event("INSTANCE_ACTIVATION_VALIDATION_STARTED", {
308
+ correlationId: req.correlationId,
309
+ });
310
+
311
+ try {
312
+ const response = await providerPost(
313
+ req,
314
+ PATHS.validar,
315
+ providerPayload(req, codigoAtivacao),
316
+ );
317
+ assertApp(response.data);
318
+ instance.status = "validando";
319
+ instance.ultimaValidacaoEm = new Date();
320
+ await instance.save();
321
+ event("INSTANCE_ACTIVATION_VALIDATION_SUCCEEDED", {
322
+ correlationId: req.correlationId,
323
+ });
324
+ sendResponse({ res, ...safeContext(response.data) });
325
+ } catch (error) {
326
+ event("INSTANCE_ACTIVATION_VALIDATION_FAILED", {
327
+ correlationId: req.correlationId,
328
+ status: error.response?.status,
329
+ });
330
+ throw error.response
331
+ ? new GenericError(
332
+ error.response.data?.message || "Código de ativação inválido.",
333
+ { statusCode: error.response.status },
334
+ )
335
+ : error;
336
+ }
337
+ }),
338
+ );
339
+
340
+ router.post(
341
+ "/concluir",
342
+ asyncHandler(async (req, res) => {
343
+ assertMember();
344
+ const {
345
+ codigoAtivacao,
346
+ nomeInstancia,
347
+ parametrosGlobais = {},
348
+ parametrosAplicacao = {},
349
+ } = req.body || {};
350
+
351
+ if (!codigoAtivacao) {
352
+ throw new GenericError("Código de ativação obrigatório.", {
353
+ statusCode: 400,
354
+ });
355
+ }
356
+
357
+ validateParams(
358
+ { ...parametrosGlobais, nomeInstancia },
359
+ parametrosAplicacao,
360
+ );
361
+
362
+ const config = activationConfig();
363
+ const instance = await ensureInstance();
364
+
365
+ if (
366
+ instance.instanceId &&
367
+ ["configurando", "erro"].includes(instance.status)
368
+ ) {
369
+ return res.redirect(307, "/ativacao/tentar-novamente");
370
+ }
371
+
372
+ if (!["nao_ativada", "validando", "erro"].includes(instance.status)) {
373
+ throw new GenericError(
374
+ "Instância não pode ser ativada neste estado.",
375
+ { statusCode: 409 },
376
+ );
377
+ }
378
+
379
+ event("INSTANCE_ACTIVATION_STARTED", {
380
+ correlationId: req.correlationId,
381
+ });
382
+
383
+ if (typeof config.hooks.validate === "function") {
384
+ await config.hooks.validate({
385
+ globalParameters: parametrosGlobais,
386
+ appParameters: parametrosAplicacao,
387
+ context: { req },
388
+ });
389
+ }
390
+
391
+ const preview = await providerPost(
392
+ req,
393
+ PATHS.validar,
394
+ providerPayload(req, codigoAtivacao),
395
+ );
396
+ assertApp(preview.data);
397
+
398
+ const activated = await providerPost(
399
+ req,
400
+ PATHS.ativar,
401
+ providerPayload(req, codigoAtivacao, { nomeInstancia, metadata: {} }),
402
+ );
403
+ assertApp(activated.data);
404
+
405
+ const token = activated.data?.credencial?.token;
406
+ if (!activated.data?.instancia?.id || !token) {
407
+ throw new GenericError("Resposta de ativação inválida.", {
408
+ statusCode: 502,
409
+ });
410
+ }
411
+
412
+ Object.assign(instance, encrypt(token), {
413
+ status: "configurando",
414
+ instanceId: activated.data.instancia.id,
415
+ instanceCode: activated.data.instancia.codigo,
416
+ credentialVersion: activated.data.credencial.versao,
417
+ appCode: getAppCode(),
418
+ appId: activated.data.app?.id || preview.data?.app?.id,
419
+ tenantId: activated.data.tenant?.id || preview.data?.tenant?.id,
420
+ licencaId: activated.data.licenca?.id || preview.data?.licenca?.id,
421
+ ambiente: ambiente(),
422
+ nomeInstancia,
423
+ publicUrl: publicUrl(req),
424
+ tenant: activated.data.tenant || preview.data?.tenant,
425
+ app: activated.data.app || preview.data?.app,
426
+ licenca: activated.data.licenca || preview.data?.licenca,
427
+ parametrosGlobais,
428
+ parametrosAplicacao,
429
+ configurationVersion: config.configurationVersion,
430
+ ativadaEm: new Date(),
431
+ ultimoErro: undefined,
432
+ });
433
+
434
+ await instance.save();
435
+ event("INSTANCE_ACTIVATION_REGISTERED", {
436
+ correlationId: req.correlationId,
437
+ });
438
+
439
+ await complete(req, instance, config);
440
+ sendResponse({
441
+ res,
442
+ sucesso: true,
443
+ status: "ativa",
444
+ instanceId: instance.instanceId,
445
+ });
446
+ }),
447
+ );
448
+
449
+ async function complete(req, instance, config) {
450
+ try {
451
+ if (typeof config.hooks.beforeComplete === "function") {
452
+ await config.hooks.beforeComplete({
453
+ globalParameters: instance.parametrosGlobais,
454
+ appParameters: instance.parametrosAplicacao,
455
+ context: { req },
456
+ });
457
+ }
458
+
459
+ await providerPost(
460
+ req,
461
+ PATHS.concluir,
462
+ {
463
+ configurationVersion: config.configurationVersion,
464
+ parametros: instance.parametrosGlobais,
465
+ },
466
+ instanceHeaders(instance),
467
+ );
468
+
469
+ instance.status = "ativa";
470
+ instance.concluidaEm = new Date();
471
+ instance.ultimoErro = undefined;
472
+ await instance.save();
473
+
474
+ event("INSTANCE_ACTIVATION_COMPLETED", {
475
+ correlationId: req.correlationId,
476
+ });
477
+
478
+ if (typeof config.hooks.afterComplete === "function") {
479
+ await config.hooks.afterComplete({
480
+ instance: instance.toJSON(),
481
+ context: { req },
482
+ });
483
+ }
484
+ } catch (error) {
485
+ instance.status = "erro";
486
+ instance.ultimoErro = error.message;
487
+ await instance.save();
488
+ event("INSTANCE_ACTIVATION_FAILED", {
489
+ correlationId: req.correlationId,
490
+ });
491
+ throw error;
492
+ }
493
+ }
494
+
495
+ router.post(
496
+ "/tentar-novamente",
497
+ asyncHandler(async (req, res) => {
498
+ assertMember();
499
+ const instance = await ensureInstance();
500
+ const config = activationConfig();
501
+
502
+ if (
503
+ !instance.instanceId ||
504
+ !["configurando", "erro"].includes(instance.status)
505
+ ) {
506
+ throw new GenericError(
507
+ "Não há ativação pendente para retomar.",
508
+ { statusCode: 409 },
509
+ );
510
+ }
511
+
512
+ await complete(req, instance, config);
513
+ sendResponse({
514
+ res,
515
+ sucesso: true,
516
+ status: instance.status,
517
+ instanceId: instance.instanceId,
518
+ });
519
+ }),
520
+ );
521
+
522
+ module.exports = router;