@oondemand/oon-core-back 0.4.29 → 0.5.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,5 +1,11 @@
1
1
  # OonCore Back - ativação de instâncias
2
2
 
3
+ ## Runtime local desconectado
4
+
5
+ Com `NODE_ENV=development` e `OON_RUNTIME_MODE=local`, o Core escuta apenas em `127.0.0.1`, cria uma sessão técnica local de até 30 dias e não consulta a Central de Ativações. Códigos independentes e de uso único atendem navegador e automação/seed. Apps com tenant usam o contexto virtual fixo `local:tenant`, sem criar um recurso. O estado é `ativa_local`; nenhuma instância, licença, Deployment ou credencial operacional é criada. Produção, Kubernetes, bind externo e identidade operacional falham no startup.
6
+
7
+ O scaffold configura esse fluxo. Não use `DEV_TOKEN` em uma Central nova.
8
+
3
9
  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
10
 
5
11
  ## Variáveis de ambiente
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.4.29",
3
+ "version": "0.5.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -7,6 +7,7 @@ const { asyncHandler, sendResponse } = require("../core/utils/helpers");
7
7
  const { ensureInstance, operationalRequestHeaders } = require("../core/utils/instanceActivation");
8
8
  const { findAccessForUser, audit } = require("../core/rbac/storage");
9
9
  const { changeFirstAccessPassword } = require("../core/rbac/identityProvider");
10
+ const { isLocalRuntime } = require("../core/runtime/localDevelopment");
10
11
  const {
11
12
  LOGIN_PATH,
12
13
  getBaseUrl,
@@ -20,6 +21,14 @@ const SESSION_SCOPE_HEADER = "x-oon-session-scope";
20
21
  const LAUNCH_EXCHANGE_PATH = "/integracao/app-access/launch/exchange";
21
22
  const router = express.Router();
22
23
 
24
+ router.use((req, _res, next) => {
25
+ if (!isLocalRuntime() || (req.method === "GET" && req.path === "/validar-token")) return next();
26
+ return next(new GenericError("Operação da plataforma indisponível no runtime local.", {
27
+ statusCode: 403,
28
+ code: "LOCAL_OPERATION_NOT_SUPPORTED",
29
+ }));
30
+ });
31
+
23
32
  function readBasicAuthorization(req) {
24
33
  const authorization = String(req.headers.authorization || "");
25
34
  if (!authorization.startsWith("Basic ")) {
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+
3
+ const express = require("express");
4
+ const auth = require("../core/middlewares/auth");
5
+ const GenericError = require("../core/errors/GenericError");
6
+ const { asyncHandler } = require("../core/utils/helpers");
7
+ const {
8
+ SESSION_COOKIE,
9
+ CSRF_COOKIE,
10
+ exchangeBootstrapCode,
11
+ validateLocalSession,
12
+ validateCsrf,
13
+ invalidateLocalSession,
14
+ localRoles,
15
+ setLocalRole,
16
+ parseCookies,
17
+ cookie,
18
+ clearCookie,
19
+ localRuntimeSummary,
20
+ } = require("../core/runtime/localDevelopment");
21
+
22
+ const router = express.Router();
23
+
24
+ router.post("/bootstrap", asyncHandler(async (req, res) => {
25
+ const code = String(req.body?.code || "").trim();
26
+ const result = await exchangeBootstrapCode(code);
27
+ const maxAge = Math.max(1, Math.floor((result.expiresAt.getTime() - Date.now()) / 1000));
28
+ res.setHeader("Set-Cookie", [
29
+ cookie(SESSION_COOKIE, result.sessionToken, { httpOnly: true, maxAge }),
30
+ cookie(CSRF_COOKIE, result.csrfToken, { maxAge }),
31
+ ]);
32
+ res.set("Cache-Control", "no-store");
33
+ res.status(200).json({ ...localRuntimeSummary(result), roleCode: result.roleCode });
34
+ }));
35
+
36
+ router.use(auth);
37
+
38
+ router.get("/context", asyncHandler(async (req, res) => {
39
+ const cookies = parseCookies(req.headers.cookie);
40
+ const session = await validateLocalSession(cookies[SESSION_COOKIE]);
41
+ res.set("Cache-Control", "no-store");
42
+ res.json({ ...localRuntimeSummary(session), roleCode: session.roleCode, roles: localRoles() });
43
+ }));
44
+
45
+ router.post("/role", asyncHandler(async (req, res) => {
46
+ const cookies = parseCookies(req.headers.cookie);
47
+ await validateCsrf(req.headers["x-oon-local-csrf"] || cookies[CSRF_COOKIE]);
48
+ const role = await setLocalRole(req.body?.roleCode);
49
+ res.set("Cache-Control", "no-store");
50
+ res.json({ role });
51
+ }));
52
+
53
+ router.post("/logout", asyncHandler(async (_req, res) => {
54
+ await invalidateLocalSession();
55
+ res.setHeader("Set-Cookie", [
56
+ clearCookie(SESSION_COOKIE, { httpOnly: true }),
57
+ clearCookie(CSRF_COOKIE),
58
+ ]);
59
+ res.set("Cache-Control", "no-store");
60
+ res.status(204).end();
61
+ }));
62
+
63
+ router.use((_req, _res, next) => next(new GenericError("Operação local desconhecida.", {
64
+ statusCode: 404,
65
+ code: "LOCAL_OPERATION_NOT_FOUND",
66
+ })));
67
+
68
+ module.exports = router;
@@ -4,6 +4,7 @@ const express = require("express");
4
4
  const mongoose = require("mongoose");
5
5
  const ctx = require("../core/context");
6
6
  const { runtimeSummary } = require("./core.routes");
7
+ const { isLocalRuntime, localRuntimeSummary } = require("../core/runtime/localDevelopment");
7
8
 
8
9
  const router = express.Router();
9
10
 
@@ -31,6 +32,7 @@ function readinessSummary(readyState = mongoose.connection.readyState) {
31
32
  ready,
32
33
  state: database.code,
33
34
  },
35
+ ...(isLocalRuntime() ? localRuntimeSummary() : { runtimeMode: "platform" }),
34
36
  };
35
37
  }
36
38
 
@@ -39,6 +41,7 @@ function versionSummary() {
39
41
  status: "ok",
40
42
  service: ctx.config.serviceName,
41
43
  ...runtimeSummary(),
44
+ ...(isLocalRuntime() ? localRuntimeSummary() : { runtimeMode: "platform" }),
42
45
  };
43
46
  }
44
47
 
@@ -59,6 +62,19 @@ router.get("/health/version", (_req, res) => {
59
62
  res.status(200).json(versionSummary());
60
63
  });
61
64
 
65
+ router.get("/ativacao/status", (_req, res, next) => {
66
+ if (!isLocalRuntime()) return next();
67
+ res.set("Cache-Control", "no-store");
68
+ res.status(200).json({
69
+ role: "member",
70
+ appCode: ctx.config.appCode || ctx.config.slug || process.env.APP_CODE,
71
+ ambiente: "desenvolvimento",
72
+ podeAtivar: false,
73
+ podeTentarNovamente: false,
74
+ ...localRuntimeSummary(),
75
+ });
76
+ });
77
+
62
78
  module.exports = router;
63
79
  module.exports.databaseState = databaseState;
64
80
  module.exports.readinessSummary = readinessSummary;
@@ -18,11 +18,32 @@ const authRoutes = require("../api/auth.routes");
18
18
  const activationRoutes = require("../api/activation.routes");
19
19
  const activationGuard = require("../core/middlewares/activationGuard");
20
20
  const transactionalEmail = require("../capabilities/transactionalEmail");
21
+ const localDevelopmentRoutes = require("../api/localDevelopment.routes");
22
+ const {
23
+ isLocalRuntime,
24
+ isLoopbackHost,
25
+ localRequestBoundary,
26
+ } = require("../core/runtime/localDevelopment");
27
+
28
+ function localCorsOptions() {
29
+ return {
30
+ credentials: true,
31
+ origin(origin, callback) {
32
+ if (!origin || isLoopbackHost(origin)) return callback(null, true);
33
+ const error = new Error("Origin não permitida no runtime local.");
34
+ error.statusCode = 403;
35
+ error.code = "LOCAL_ORIGIN_FORBIDDEN";
36
+ return callback(error);
37
+ },
38
+ };
39
+ }
21
40
 
22
41
  function createApp() {
23
42
  const app = express();
24
43
 
25
- app.use(cors(ctx.config.cors));
44
+ app.disable("trust proxy");
45
+ app.use(localRequestBoundary);
46
+ app.use(cors(isLocalRuntime() ? localCorsOptions() : ctx.config.cors));
26
47
  app.use(helmet());
27
48
  app.use(express.json({ limit: "10mb" }));
28
49
  app.use(express.urlencoded({ extended: true, limit: "10mb" }));
@@ -30,7 +51,8 @@ function createApp() {
30
51
  if (process.env.NODE_ENV === "development") app.use(morgan("dev"));
31
52
 
32
53
  app.use("/", statusRoutes);
33
- app.use("/ativacao", activationRoutes);
54
+ if (isLocalRuntime()) app.use("/auth/local", localDevelopmentRoutes);
55
+ else app.use("/ativacao", activationRoutes);
34
56
  app.use(activationGuard);
35
57
  app.use("/auth", authRoutes);
36
58
  app.use("/core", accessRoutes);
@@ -3,6 +3,11 @@ const { connectDB } = require("./database");
3
3
  const { createApp } = require("./createApp");
4
4
  const { loadCentral } = require("./loadCentral");
5
5
  const transactionalEmail = require("../capabilities/transactionalEmail");
6
+ const {
7
+ isLocalRuntime,
8
+ validateLocalRuntime,
9
+ prepareLocalDevelopmentSession,
10
+ } = require("../core/runtime/localDevelopment");
6
11
 
7
12
  /**
8
13
  * Sobe a aplicação completa: carrega a Central do consumidor, conecta no
@@ -15,14 +20,23 @@ async function start({ cwd = process.cwd(), listen = true } = {}) {
15
20
  loadCentral(cwd);
16
21
  await connectDB();
17
22
  await transactionalEmail.initialize();
23
+ const host = isLocalRuntime() ? "127.0.0.1" : undefined;
24
+ validateLocalRuntime({ host: host || process.env.HOST || "127.0.0.1" });
25
+ const localSession = isLocalRuntime() ? await prepareLocalDevelopmentSession() : null;
18
26
  const app = createApp();
19
27
 
20
28
  if (!listen) return { app };
21
29
 
22
30
  const port = ctx.config.port;
23
31
  return new Promise((resolve) => {
24
- const server = app.listen(port, () => {
32
+ const server = app.listen(port, host, () => {
25
33
  console.log("****************************************************************");
34
+ if (localSession) {
35
+ console.log("Runtime: desenvolvimento local desconectado");
36
+ console.log(`URL: http://127.0.0.1:5173/?oon_local_bootstrap=${localSession.bootstrapCode}`);
37
+ console.log(`Código para automação/seed: ${localSession.automationBootstrapCode}`);
38
+ console.log(`Expira em: ${localSession.expiresAt.toISOString()}`);
39
+ }
26
40
  console.log(`${ctx.config.serviceName} rodando na porta ${port} e conectado ao MongoDB`);
27
41
  console.log("****************************************************************");
28
42
  const stop = () => transactionalEmail.stopWorker();
@@ -1,5 +1,6 @@
1
1
  const { ensureInstance } = require("../utils/instanceActivation");
2
2
  const Helpers = require("../utils/helpers");
3
+ const { isLocalRuntime } = require("../runtime/localDevelopment");
3
4
 
4
5
  const allowed = [
5
6
  /^\/ativacao(\/|$)/,
@@ -10,6 +11,7 @@ const allowed = [
10
11
 
11
12
  module.exports = async function activationGuard(req, res, next) {
12
13
  try {
14
+ if (isLocalRuntime()) return next();
13
15
  if (allowed.some((pattern) => pattern.test(req.path))) return next();
14
16
 
15
17
  const instance = await ensureInstance();
@@ -13,6 +13,13 @@ const {
13
13
  readRequestedTenantId,
14
14
  createAccessContext,
15
15
  } = require("../tenancy");
16
+ const {
17
+ SESSION_COOKIE,
18
+ isLocalRuntime,
19
+ parseCookies,
20
+ validateLocalSession,
21
+ localPrincipal,
22
+ } = require("../runtime/localDevelopment");
16
23
 
17
24
  async function defaultVerifyToken(token, { req } = {}) {
18
25
  const baseUrl = getBaseUrl(req);
@@ -89,6 +96,7 @@ function localDevelopmentVerifyToken(token, { req } = {}) {
89
96
  }
90
97
 
91
98
  function resolveVerifyToken() {
99
+ if (isLocalRuntime()) return async (token) => localPrincipal(await validateLocalSession(token));
92
100
  if (typeof ctx.config.auth?.verifyToken === "function") return ctx.config.auth.verifyToken;
93
101
  if (process.env.NODE_ENV === "development" && process.env.DEV_TOKEN) {
94
102
  return localDevelopmentVerifyToken;
@@ -102,6 +110,15 @@ function isFirstAccessRoute(req) {
102
110
  }
103
111
 
104
112
  async function applyEffectiveAccess(usuario) {
113
+ if (usuario?.localDevelopment && usuario?.runtimeMode === "local") {
114
+ return {
115
+ ...usuario,
116
+ accessStatus: "active",
117
+ mustChangePassword: false,
118
+ firstAccessPending: false,
119
+ rbacMode: "local-development",
120
+ };
121
+ }
105
122
  const access = await resolveUserAccess(usuario);
106
123
  return {
107
124
  ...usuario,
@@ -115,7 +132,9 @@ async function applyEffectiveAccess(usuario) {
115
132
  }
116
133
 
117
134
  const authMiddleware = async (req, res, next) => {
118
- const token = req.headers.authorization?.split(" ")[1];
135
+ const token = isLocalRuntime()
136
+ ? parseCookies(req.headers.cookie)[SESSION_COOKIE]
137
+ : req.headers.authorization?.split(" ")[1];
119
138
  if (!token) {
120
139
  return Helpers.sendErrorResponse({
121
140
  res,
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const mongoose = require("mongoose");
5
+ const ctx = require("../context");
6
+ const GenericError = require("../errors/GenericError");
7
+ const { policy, roleByCode, rolePermissions } = require("../rbac/policy");
8
+ const { resolveLocalExecutionPolicy } = require("./localExecutionPolicy");
9
+
10
+ const RUNTIME_MODE_ENV = "OON_RUNTIME_MODE";
11
+ const LOCAL_RUNTIME_MODE = "local";
12
+ const DEFAULT_TTL_DAYS = 30;
13
+ const BOOTSTRAP_TTL_MINUTES = 10;
14
+ const SESSION_COOKIE = "oon_local_session";
15
+ const CSRF_COOKIE = "oon_local_csrf";
16
+ const CSRF_HEADER = "x-oon-local-csrf";
17
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
18
+
19
+ let memorySession = null;
20
+ let preparedSecrets = null;
21
+
22
+ function isLocalRuntime(env = process.env) {
23
+ return String(env[RUNTIME_MODE_ENV] || "").trim().toLowerCase() === LOCAL_RUNTIME_MODE;
24
+ }
25
+
26
+ function runtimeMode(env = process.env) {
27
+ return isLocalRuntime(env) ? LOCAL_RUNTIME_MODE : "platform";
28
+ }
29
+
30
+ function hash(value) {
31
+ return crypto.createHash("sha256").update(String(value || "")).digest("hex");
32
+ }
33
+
34
+ function randomSecret() {
35
+ return crypto.randomBytes(32).toString("base64url");
36
+ }
37
+
38
+ function safeEqualHash(left, right) {
39
+ const a = Buffer.from(String(left || ""), "hex");
40
+ const b = Buffer.from(String(right || ""), "hex");
41
+ return a.length > 0 && a.length === b.length && crypto.timingSafeEqual(a, b);
42
+ }
43
+
44
+ function hostnameOf(value) {
45
+ const raw = String(value || "").trim();
46
+ if (!raw) return "";
47
+ try {
48
+ return new URL(raw.includes("://") ? raw : `http://${raw}`).hostname.replace(/^\[|\]$/g, "").toLowerCase();
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function isLoopbackHost(value) {
55
+ return LOOPBACK_HOSTS.has(hostnameOf(value));
56
+ }
57
+
58
+ function localError(message, code, statusCode = 400) {
59
+ return new GenericError(message, { code, statusCode });
60
+ }
61
+
62
+ function validateLocalRuntime({ env = process.env, host = "127.0.0.1" } = {}) {
63
+ if (!isLocalRuntime(env)) return { runtimeMode: "platform", host };
64
+ if (env.NODE_ENV !== "development") {
65
+ throw localError("O runtime local exige NODE_ENV=development.", "LOCAL_RUNTIME_ENV_INVALID", 500);
66
+ }
67
+ if (env.KUBERNETES_SERVICE_HOST || env.OON_DEPLOYMENT_ID || env.OON_DEPLOYMENT_TOKEN || env.OON_INSTANCE_ID || env.OON_INSTANCE_TOKEN) {
68
+ throw localError("O runtime local não pode iniciar com identidade de Deployment, instância ou Kubernetes.", "LOCAL_RUNTIME_PLATFORM_IDENTITY", 500);
69
+ }
70
+ if (!isLoopbackHost(host)) {
71
+ throw localError("O runtime local deve escutar exclusivamente em 127.0.0.1, localhost ou ::1.", "LOCAL_RUNTIME_BIND_FORBIDDEN", 500);
72
+ }
73
+ if (env.PUBLIC_APP_URL && !isLoopbackHost(env.PUBLIC_APP_URL)) {
74
+ throw localError("PUBLIC_APP_URL deve apontar para loopback no runtime local.", "LOCAL_RUNTIME_PUBLIC_URL_FORBIDDEN", 500);
75
+ }
76
+ return { runtimeMode: LOCAL_RUNTIME_MODE, host: hostnameOf(host) };
77
+ }
78
+
79
+ function ttlDays(env = process.env, policyTtlDays = DEFAULT_TTL_DAYS) {
80
+ const parsed = Number(env.OON_LOCAL_TTL_DAYS || policyTtlDays);
81
+ return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, policyTtlDays, 30) : policyTtlDays;
82
+ }
83
+
84
+ function localSessionModel() {
85
+ const schema = new mongoose.Schema({
86
+ singletonKey: { type: String, default: "default", unique: true, immutable: true },
87
+ tokenHash: { type: String, required: true, select: false },
88
+ csrfHash: { type: String, required: true, select: false },
89
+ bootstrapHash: { type: String, required: true, select: false },
90
+ automationBootstrapHash: { type: String, required: true, select: false },
91
+ bootstrapExpiresAt: { type: Date, required: true },
92
+ expiresAt: { type: Date, required: true, index: true },
93
+ roleCode: { type: String, required: true },
94
+ }, { timestamps: true, collection: "oon_core_local_development_session" });
95
+ return mongoose.models.OonCoreLocalDevelopmentSession
96
+ || mongoose.model("OonCoreLocalDevelopmentSession", schema);
97
+ }
98
+
99
+ function defaultRoleCode() {
100
+ const current = policy();
101
+ if (!current.enabled) return "admin";
102
+ return roleByCode("developer")?.code
103
+ || current.roles.find((role) => role.admin)?.code
104
+ || current.roles[0]?.code
105
+ || "admin";
106
+ }
107
+
108
+ async function loadSession() {
109
+ if (mongoose.connection.readyState !== 1) return memorySession;
110
+ return localSessionModel().findOne({ singletonKey: "default" })
111
+ .select("+tokenHash +csrfHash +bootstrapHash +automationBootstrapHash");
112
+ }
113
+
114
+ async function persistSession(values, existing) {
115
+ if (mongoose.connection.readyState !== 1) {
116
+ memorySession = { ...(existing || {}), ...values, singletonKey: "default" };
117
+ return memorySession;
118
+ }
119
+ if (existing) {
120
+ Object.assign(existing, values);
121
+ await existing.save();
122
+ return existing;
123
+ }
124
+ return localSessionModel().create({ singletonKey: "default", ...values });
125
+ }
126
+
127
+ async function prepareLocalDevelopmentSession({ now = new Date() } = {}) {
128
+ if (!isLocalRuntime()) return null;
129
+ validateLocalRuntime({ host: process.env.HOST || "127.0.0.1" });
130
+ const executionPolicy = await resolveLocalExecutionPolicy({ appCode: ctx.config.appCode || ctx.config.slug });
131
+ const current = await loadSession();
132
+ const currentExpiry = current?.expiresAt ? new Date(current.expiresAt) : null;
133
+ if (currentExpiry && currentExpiry <= now) {
134
+ throw localError(
135
+ "O período de desenvolvimento local expirou. Exclua os dados locais para iniciar um novo período.",
136
+ "LOCAL_SESSION_EXPIRED",
137
+ 403,
138
+ );
139
+ }
140
+ const expiresAt = currentExpiry
141
+ || new Date(now.getTime() + ttlDays(process.env, executionPolicy.ttlDays) * 24 * 60 * 60 * 1000);
142
+ const sessionToken = randomSecret();
143
+ const csrfToken = randomSecret();
144
+ const configuredBootstrap = String(process.env.OON_LOCAL_BOOTSTRAP_CODE || "").trim();
145
+ const bootstrapCode = configuredBootstrap.length >= 43 ? configuredBootstrap : randomSecret();
146
+ const configuredAutomationBootstrap = String(process.env.OON_LOCAL_AUTOMATION_BOOTSTRAP_CODE || "").trim();
147
+ const automationBootstrapCode = configuredAutomationBootstrap.length >= 43
148
+ ? configuredAutomationBootstrap
149
+ : randomSecret();
150
+ const values = {
151
+ tokenHash: hash(sessionToken),
152
+ csrfHash: hash(csrfToken),
153
+ bootstrapHash: hash(bootstrapCode),
154
+ automationBootstrapHash: hash(automationBootstrapCode),
155
+ bootstrapExpiresAt: new Date(now.getTime() + BOOTSTRAP_TTL_MINUTES * 60 * 1000),
156
+ expiresAt,
157
+ roleCode: current?.roleCode || defaultRoleCode(),
158
+ };
159
+ await persistSession(values, current);
160
+ preparedSecrets = { sessionToken, csrfToken, bootstrapCode, automationBootstrapCode, expiresAt };
161
+ return { bootstrapCode, automationBootstrapCode, expiresAt, roleCode: values.roleCode, policy: executionPolicy };
162
+ }
163
+
164
+ async function exchangeBootstrapCode(code, { now = new Date() } = {}) {
165
+ const session = await loadSession();
166
+ const providedHash = hash(code);
167
+ const secretName = session && safeEqualHash(session.bootstrapHash, providedHash)
168
+ ? "bootstrapCode"
169
+ : session && safeEqualHash(session.automationBootstrapHash, providedHash)
170
+ ? "automationBootstrapCode"
171
+ : null;
172
+ if (!session || !preparedSecrets || session.bootstrapExpiresAt <= now || !secretName) {
173
+ throw localError("Código de inicialização local inválido ou expirado.", "LOCAL_BOOTSTRAP_INVALID", 401);
174
+ }
175
+ const consumedField = secretName === "bootstrapCode" ? "bootstrapHash" : "automationBootstrapHash";
176
+ const replacementHash = hash(randomSecret());
177
+ await persistSession({ [consumedField]: replacementHash }, session);
178
+ const result = {
179
+ sessionToken: preparedSecrets.sessionToken,
180
+ csrfToken: preparedSecrets.csrfToken,
181
+ expiresAt: preparedSecrets.expiresAt,
182
+ roleCode: session.roleCode,
183
+ };
184
+ preparedSecrets = { ...preparedSecrets, [secretName]: null };
185
+ return result;
186
+ }
187
+
188
+ async function validateLocalSession(token, { now = new Date() } = {}) {
189
+ const session = await loadSession();
190
+ if (!session || !token || session.expiresAt <= now || !safeEqualHash(session.tokenHash, hash(token))) {
191
+ throw localError("Sessão local ausente, inválida ou expirada.", "LOCAL_SESSION_INVALID", 401);
192
+ }
193
+ return session;
194
+ }
195
+
196
+ async function validateCsrf(token) {
197
+ const session = await loadSession();
198
+ if (!session || !token || !safeEqualHash(session.csrfHash, hash(token))) {
199
+ throw localError("Proteção CSRF local inválida.", "LOCAL_CSRF_INVALID", 403);
200
+ }
201
+ return true;
202
+ }
203
+
204
+ async function invalidateLocalSession({ now = new Date() } = {}) {
205
+ const session = await loadSession();
206
+ if (!session) return;
207
+ await persistSession({
208
+ tokenHash: hash(randomSecret()),
209
+ csrfHash: hash(randomSecret()),
210
+ bootstrapHash: hash(randomSecret()),
211
+ automationBootstrapHash: hash(randomSecret()),
212
+ bootstrapExpiresAt: now,
213
+ }, session);
214
+ preparedSecrets = null;
215
+ }
216
+
217
+ function localRoles() {
218
+ const current = policy();
219
+ if (!current.enabled) return [{ code: "admin", name: "Administrador local", permissions: ["*"] }];
220
+ return current.roles.map((role) => ({
221
+ code: role.code,
222
+ name: role.name || role.code,
223
+ permissions: rolePermissions(role),
224
+ admin: Boolean(role.admin),
225
+ }));
226
+ }
227
+
228
+ async function setLocalRole(roleCode) {
229
+ const role = localRoles().find((candidate) => candidate.code === String(roleCode || ""));
230
+ if (!role) throw localError("Perfil local não está declarado no central.app.json.", "LOCAL_ROLE_NOT_DECLARED", 422);
231
+ const session = await loadSession();
232
+ if (!session) throw localError("Sessão local não inicializada.", "LOCAL_SESSION_INVALID", 401);
233
+ await persistSession({ roleCode: role.code }, session);
234
+ return role;
235
+ }
236
+
237
+ async function localPrincipal(session) {
238
+ const role = localRoles().find((candidate) => candidate.code === session.roleCode) || localRoles()[0];
239
+ const tenancyModel = ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel || "single_tenant";
240
+ const tenantId = tenancyModel === "none" ? undefined : "local:tenant";
241
+ return {
242
+ id: "local:developer",
243
+ usuarioId: "local:developer",
244
+ tipo: role.code,
245
+ perfil: role.code,
246
+ roleCode: role.code,
247
+ nome: "Desenvolvedor local",
248
+ email: "developer@localhost",
249
+ permissions: role.permissions,
250
+ capabilities: ctx.config.capabilities || [],
251
+ appCode: ctx.config.appCode || ctx.config.slug || process.env.APP_CODE,
252
+ tenancyModel,
253
+ tenantId,
254
+ tenant: tenantId ? { id: tenantId, name: "Tenant local" } : undefined,
255
+ runtimeMode: LOCAL_RUNTIME_MODE,
256
+ localDevelopment: true,
257
+ };
258
+ }
259
+
260
+ function parseCookies(header) {
261
+ return String(header || "").split(";").reduce((cookies, part) => {
262
+ const index = part.indexOf("=");
263
+ if (index < 0) return cookies;
264
+ const key = part.slice(0, index).trim();
265
+ const value = part.slice(index + 1).trim();
266
+ if (key) {
267
+ try { cookies[key] = decodeURIComponent(value); }
268
+ catch { cookies[key] = ""; }
269
+ }
270
+ return cookies;
271
+ }, {});
272
+ }
273
+
274
+ function cookie(name, value, { httpOnly = false, maxAge = 0 } = {}) {
275
+ const parts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Strict"];
276
+ if (httpOnly) parts.push("HttpOnly");
277
+ if (process.env.OON_LOCAL_HTTPS === "true") parts.push("Secure");
278
+ if (maxAge) parts.push(`Max-Age=${Math.max(0, Math.floor(maxAge))}`);
279
+ return parts.join("; ");
280
+ }
281
+
282
+ function clearCookie(name, { httpOnly = false } = {}) {
283
+ return cookie(name, "", { httpOnly, maxAge: -1 }) + "; Expires=Thu, 01 Jan 1970 00:00:00 GMT";
284
+ }
285
+
286
+ function assertLocalRequest(req) {
287
+ if (!isLoopbackHost(req.headers.host)) {
288
+ throw localError("Host não permitido no runtime local.", "LOCAL_HOST_FORBIDDEN", 403);
289
+ }
290
+ if (req.headers["x-forwarded-host"] || req.headers["x-forwarded-proto"] || req.headers.forwarded) {
291
+ throw localError("Cabeçalhos de proxy não são aceitos no runtime local.", "LOCAL_PROXY_FORBIDDEN", 403);
292
+ }
293
+ const origin = req.headers.origin;
294
+ if (origin && !isLoopbackHost(origin)) {
295
+ throw localError("Origin não permitida no runtime local.", "LOCAL_ORIGIN_FORBIDDEN", 403);
296
+ }
297
+ }
298
+
299
+ async function localRequestBoundary(req, _res, next) {
300
+ if (!isLocalRuntime()) return next();
301
+ try {
302
+ assertLocalRequest(req);
303
+ const safeMethod = ["GET", "HEAD", "OPTIONS"].includes(req.method);
304
+ const bootstrap = String(req.path || "").split("?")[0] === "/auth/local/bootstrap";
305
+ if (!safeMethod && !bootstrap) {
306
+ const cookies = parseCookies(req.headers.cookie);
307
+ const csrfHeader = req.headers[CSRF_HEADER];
308
+ if (!csrfHeader || csrfHeader !== cookies[CSRF_COOKIE]) {
309
+ throw localError("Proteção CSRF local ausente ou divergente.", "LOCAL_CSRF_INVALID", 403);
310
+ }
311
+ await validateCsrf(csrfHeader);
312
+ }
313
+ return next();
314
+ } catch (error) {
315
+ return next(error);
316
+ }
317
+ }
318
+
319
+ function localRuntimeSummary(session) {
320
+ return {
321
+ runtimeMode: LOCAL_RUNTIME_MODE,
322
+ status: "ativa_local",
323
+ disconnected: true,
324
+ expiresAt: session?.expiresAt || preparedSecrets?.expiresAt,
325
+ };
326
+ }
327
+
328
+ function resetLocalDevelopmentForTests() {
329
+ memorySession = null;
330
+ preparedSecrets = null;
331
+ }
332
+
333
+ module.exports = {
334
+ RUNTIME_MODE_ENV, LOCAL_RUNTIME_MODE, SESSION_COOKIE, CSRF_COOKIE, CSRF_HEADER,
335
+ isLocalRuntime, runtimeMode, isLoopbackHost, validateLocalRuntime, ttlDays,
336
+ prepareLocalDevelopmentSession, exchangeBootstrapCode, validateLocalSession,
337
+ validateCsrf, invalidateLocalSession, localRoles, setLocalRole, localPrincipal, parseCookies, cookie,
338
+ clearCookie, assertLocalRequest, localRequestBoundary, localRuntimeSummary,
339
+ resetLocalDevelopmentForTests,
340
+ };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ const GenericError = require("../errors/GenericError");
4
+
5
+ const EMBEDDED_POLICY = Object.freeze({
6
+ schemaVersion: 1,
7
+ source: "embedded",
8
+ allowed: true,
9
+ ttlDays: 30,
10
+ offlineBehavior: "allow-until-expiration",
11
+ });
12
+
13
+ class EmbeddedLocalExecutionPolicyProvider {
14
+ async resolve() {
15
+ return { ...EMBEDDED_POLICY };
16
+ }
17
+ }
18
+
19
+ let provider = new EmbeddedLocalExecutionPolicyProvider();
20
+
21
+ function validatePolicy(value) {
22
+ if (!value || value.schemaVersion !== 1 || typeof value.allowed !== "boolean") {
23
+ throw new GenericError("Política de execução local inválida.", {
24
+ statusCode: 500,
25
+ code: "LOCAL_POLICY_INVALID",
26
+ });
27
+ }
28
+ const ttlDays = Number(value.ttlDays);
29
+ if (!Number.isFinite(ttlDays) || ttlDays <= 0 || ttlDays > 30) {
30
+ throw new GenericError("TTL da política local deve estar entre 1 e 30 dias.", {
31
+ statusCode: 500,
32
+ code: "LOCAL_POLICY_TTL_INVALID",
33
+ });
34
+ }
35
+ return { ...value, ttlDays };
36
+ }
37
+
38
+ async function resolveLocalExecutionPolicy(context = {}) {
39
+ const resolved = validatePolicy(await provider.resolve({ offline: true, ...context }));
40
+ if (!resolved.allowed) {
41
+ throw new GenericError("A política atual não permite iniciar uma nova sessão local.", {
42
+ statusCode: 403,
43
+ code: "LOCAL_EXECUTION_NOT_ALLOWED",
44
+ });
45
+ }
46
+ return resolved;
47
+ }
48
+
49
+ function setLocalExecutionPolicyProvider(nextProvider) {
50
+ if (!nextProvider || typeof nextProvider.resolve !== "function") {
51
+ throw new TypeError("LocalExecutionPolicyProvider deve implementar resolve(context).");
52
+ }
53
+ provider = nextProvider;
54
+ }
55
+
56
+ function resetLocalExecutionPolicyProvider() {
57
+ provider = new EmbeddedLocalExecutionPolicyProvider();
58
+ }
59
+
60
+ module.exports = {
61
+ EMBEDDED_POLICY,
62
+ EmbeddedLocalExecutionPolicyProvider,
63
+ resolveLocalExecutionPolicy,
64
+ setLocalExecutionPolicyProvider,
65
+ resetLocalExecutionPolicyProvider,
66
+ };
@@ -10,6 +10,7 @@ const {
10
10
  getAppCode,
11
11
  getTimeout,
12
12
  } = require("./activationProvider");
13
+ const { isLocalRuntime } = require("../runtime/localDevelopment");
13
14
 
14
15
  const PATHS = {
15
16
  validar: "/integracao/ativacao-instancias/validar-codigo",
@@ -251,6 +252,16 @@ function clearInstanceIdentity(instance) {
251
252
  }
252
253
 
253
254
  async function ensureInstance() {
255
+ if (isLocalRuntime()) {
256
+ return {
257
+ ecosystemRole: role(),
258
+ status: "ativa_local",
259
+ runtimeMode: "local",
260
+ appCode: getAppCode(),
261
+ ambiente: "desenvolvimento",
262
+ save: async () => {},
263
+ };
264
+ }
254
265
  if (mongoose.connection.readyState !== 1) {
255
266
  return {
256
267
  ecosystemRole: role(),
@@ -472,6 +483,12 @@ function operationalIdentity(instance) {
472
483
  }
473
484
 
474
485
  async function operationalRequestHeaders(options = {}, dependencies = {}) {
486
+ if (isLocalRuntime()) {
487
+ throw new GenericError("Operações da plataforma não estão disponíveis no runtime local.", {
488
+ statusCode: 403,
489
+ code: "LOCAL_OPERATION_NOT_SUPPORTED",
490
+ });
491
+ }
475
492
  const loadInstance = dependencies.ensureInstance || ensureInstance;
476
493
  const buildHeaders = dependencies.instanceHeaders || instanceHeaders;
477
494
  const instance = await loadInstance();
package/src/index.js CHANGED
@@ -20,6 +20,8 @@ const domainRules = require("./manifest/domainRules");
20
20
  const processManifest = require("./manifest/processManifest");
21
21
  const processRuntime = require("./manifest/processRuntime");
22
22
  const referencePolicies = require("./manifest/referencePolicies");
23
+ const localDevelopment = require("./core/runtime/localDevelopment");
24
+ const localExecutionPolicy = require("./core/runtime/localExecutionPolicy");
23
25
 
24
26
  module.exports = {
25
27
  start, createApp, activate,
@@ -88,6 +90,8 @@ module.exports = {
88
90
  receiveIntegrationWebhook: integrations.receiveWebhook,
89
91
  capabilities,
90
92
  operationalRequestHeaders,
93
+ localDevelopment,
94
+ localExecutionPolicy,
91
95
  PdfRenderingError: capabilities.PdfRenderingError,
92
96
  TransactionalEmailError: capabilities.TransactionalEmailError,
93
97
  GenericError, registry,