@oondemand/oon-core-back 0.7.10 → 0.7.12

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.7.10",
3
+ "version": "0.7.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -637,18 +637,51 @@ router.post(
637
637
  });
638
638
  }
639
639
 
640
- const preview = await activationProviderPost(
641
- req,
642
- PATHS.validar,
643
- providerPayload(req, codigoAtivacao),
644
- );
640
+ const payload = providerPayload(req, codigoAtivacao, { nomeInstancia, metadata: {} });
641
+ const crypto = require("node:crypto");
642
+ const { fingerprint: _fingerprint, coreVersion: _coreVersion, appVersion: _appVersion, ...semanticPayload } = payload;
643
+ const payloadHash = crypto.createHash("sha256").update(JSON.stringify({ payload: semanticPayload, parametrosGlobais, parametrosAplicacao,
644
+ configurationVersion: config.configurationVersion })).digest("hex");
645
+ const requestedKey = req.headers["idempotency-key"];
646
+ if (requestedKey && (typeof requestedKey !== "string" || requestedKey.length < 8 || requestedKey.length > 120))
647
+ throw new GenericError("Chave de ativação inválida.", { statusCode: 422, code: "IDEMPOTENCY_KEY_REQUIRED" });
648
+ if (instance.activationRequest && (instance.activationRequest.payloadHash !== payloadHash
649
+ || requestedKey && requestedKey !== instance.activationRequest.key))
650
+ throw new GenericError("Retome a mesma intenção de ativação.", { statusCode: 409, code: "IDEMPOTENCY_CONFLICT" });
651
+ const preview = instance.activationRequest ? { data: {} } : await activationProviderPost(
652
+ req, PATHS.validar, providerPayload(req, codigoAtivacao));
645
653
  assertApp(preview.data);
646
654
 
647
- const activated = await activationProviderPost(
648
- req,
649
- PATHS.ativar,
650
- providerPayload(req, codigoAtivacao, { nomeInstancia, metadata: {} }),
651
- );
655
+ // Persist identity before the provider can consume the activation code.
656
+ // A lost reply retries that command, including after process restart.
657
+ // Keep runtime-generated fields across replica/container replacement. The
658
+ // activation code itself is supplied again by the caller, never stored here.
659
+ const { codigoAtivacao: _code, ...providerContext } = payload;
660
+ const intent = instance.activationRequest || { key: requestedKey || crypto.randomUUID(), payloadHash, providerContext };
661
+ const Instance = require("../core/models/InstanciaEcossistema");
662
+ await Instance.updateOne({ _id: instance._id, activationRequest: null }, { $set: { activationRequest: intent } });
663
+ const saved = await Instance.findById(instance._id).select("+activationRequest").lean();
664
+ if (!saved || saved.activationRequest?.payloadHash !== payloadHash || requestedKey && saved.activationRequest.key !== requestedKey)
665
+ throw new GenericError("Outra intenção de ativação está pendente.", { statusCode: 409, code: "IDEMPOTENCY_CONFLICT" });
666
+ instance.activationRequest = saved.activationRequest;
667
+
668
+ let activated;
669
+ try {
670
+ activated = await activationProviderPost(req, PATHS.ativar,
671
+ { ...instance.activationRequest.providerContext, codigoAtivacao },
672
+ { "Idempotency-Key": instance.activationRequest.key });
673
+ } catch (error) {
674
+ // Only an explicit, durable provider outcome can release the local intent.
675
+ // Timeouts, generic 4xx/5xx and a used/revoked credential remain uncertain.
676
+ if (error.code === "ACTIVATION_NOT_COMMITTED"
677
+ && error.details?.operationId === instance.activationRequest.key
678
+ && error.details?.activationOutcome === "not_committed") {
679
+ await Instance.updateOne({ _id: instance._id, "activationRequest.key": instance.activationRequest.key,
680
+ "activationRequest.payloadHash": payloadHash, deploymentId: null, instanceId: null },
681
+ { $unset: { activationRequest: 1 } });
682
+ }
683
+ throw error;
684
+ }
652
685
  assertApp(activated.data);
653
686
 
654
687
  const token = activated.data?.credencial?.token || activated.data?.deploymentCredential?.token;
@@ -688,6 +721,7 @@ router.post(
688
721
  parametrosGlobais,
689
722
  parametrosAplicacao,
690
723
  configurationVersion: config.configurationVersion,
724
+ publicationIdentity: activated.data.publicationIdentity,
691
725
  ativadaEm: new Date(),
692
726
  ultimoErro: undefined,
693
727
  });
@@ -724,8 +758,11 @@ async function complete(req, instance, config) {
724
758
  {
725
759
  configurationVersion: config.configurationVersion,
726
760
  parametros: instance.parametrosGlobais,
761
+ ...(instance.publicationIdentity ? { publicationIdentity: instance.publicationIdentity } : {}),
727
762
  },
728
- instanceHeaders(instance),
763
+ { ...instanceHeaders(instance), ...(instance.publicationIdentity ? {
764
+ "Idempotency-Key": `configuration:${instance.publicationIdentity.releaseId}:${config.configurationVersion}`,
765
+ } : {}) },
729
766
  );
730
767
 
731
768
  instance.status = "ativa";
@@ -30,6 +30,8 @@ const schema = new mongoose.Schema(
30
30
  credentialAuthTag: { type: String, select: false },
31
31
  credentialVersion: Number,
32
32
  configurationVersion: Number,
33
+ publicationIdentity: { type: mongoose.Schema.Types.Mixed },
34
+ activationRequest: { type: mongoose.Schema.Types.Mixed, select: false },
33
35
  parametrosGlobais: { type: mongoose.Schema.Types.Mixed, default: {} },
34
36
  parametrosAplicacao: { type: mongoose.Schema.Types.Mixed, default: {} },
35
37
  tenant: { type: mongoose.Schema.Types.Mixed, default: {} },
@@ -54,6 +56,7 @@ const schema = new mongoose.Schema(
54
56
  delete ret.credentialEncrypted;
55
57
  delete ret.credentialIv;
56
58
  delete ret.credentialAuthTag;
59
+ delete ret.activationRequest;
57
60
  return ret;
58
61
  },
59
62
  },
@@ -127,9 +127,14 @@ function deploymentBootstrap() {
127
127
  }
128
128
  const tenancyModel =
129
129
  ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel;
130
- if (tenancyModel !== "none") {
130
+ const shared = tenancyModel === "multi_tenant"
131
+ && (ctx.config.deploymentMode || ctx.config.ecosystem?.deploymentMode) === "shared"
132
+ && (ctx.config.availability || ctx.config.ecosystem?.availability) === "common"
133
+ && (ctx.config.accessMode || ctx.config.ecosystem?.accessMode) === "tenant_members"
134
+ && (ctx.config.authorizationMode || ctx.config.ecosystem?.authorizationMode) === "rbac";
135
+ if (tenancyModel !== "none" && !shared) {
131
136
  throw new GenericError(
132
- "Bootstrap direto de deployment é permitido somente para Apps globais.",
137
+ "Bootstrap direto exige App global ou comum multi-tenant/shared.",
133
138
  { statusCode: 500, code: "DEPLOYMENT_BOOTSTRAP_TENANCY_INVALID" },
134
139
  );
135
140
  }
@@ -249,6 +254,8 @@ function clearInstanceIdentity(instance) {
249
254
  instance.credentialEncrypted = undefined;
250
255
  instance.credentialIv = undefined;
251
256
  instance.credentialAuthTag = undefined;
257
+ instance.publicationIdentity = undefined;
258
+ instance.activationRequest = undefined;
252
259
  }
253
260
 
254
261
  async function ensureInstance() {
@@ -273,7 +280,7 @@ async function ensureInstance() {
273
280
  }
274
281
 
275
282
  let instance = await Instancia.findOne().select(
276
- "+credentialEncrypted +credentialIv +credentialAuthTag",
283
+ "+credentialEncrypted +credentialIv +credentialAuthTag +activationRequest",
277
284
  );
278
285
 
279
286
  if (!instance) {
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ const localDevelopment = require("./core/runtime/localDevelopment");
23
23
  const localExecutionPolicy = require("./core/runtime/localExecutionPolicy");
24
24
 
25
25
  module.exports = {
26
+ appArchitecture: require("./manifest/appArchitecture"),
26
27
  start, createApp, activate,
27
28
  defineCentral: define.defineCentral, defineModel: define.defineModel,
28
29
  defineCollection: define.defineCollection, defineDocument: define.defineDocument,
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ // Canonical #290 contract. Deployment is a derived technical projection, never
4
+ // a second architectural authority. This module has no commercial policy.
5
+ const DEPLOYMENT_BY_TENANCY = Object.freeze({ none: "singleton", single_tenant: "dedicated", multi_tenant: "shared" });
6
+ const AVAILABILITY_TENANCY = Object.freeze({
7
+ system: Object.freeze(["none"]), common: Object.freeze(["none", "multi_tenant"]),
8
+ self: Object.freeze(["single_tenant"]), licensed: Object.freeze(["single_tenant", "multi_tenant"]),
9
+ });
10
+ function invalid(code, field, message) {
11
+ throw Object.assign(new Error(message), { code, statusCode: 422, details: { field } });
12
+ }
13
+ function deriveDeploymentMode(tenancyModel) {
14
+ if (!Object.hasOwn(DEPLOYMENT_BY_TENANCY, tenancyModel)) invalid("APP_TENANCY_INVALID", "tenancyModel", "Modelo de tenancy inválido.");
15
+ return DEPLOYMENT_BY_TENANCY[tenancyModel];
16
+ }
17
+ function resolveAppArchitecture(input) {
18
+ const { availability, tenancyModel } = input || {};
19
+ const deploymentMode = deriveDeploymentMode(tenancyModel);
20
+ if (!Object.hasOwn(AVAILABILITY_TENANCY, availability) || !AVAILABILITY_TENANCY[availability].includes(tenancyModel)) {
21
+ invalid("APP_ARCHITECTURE_INVALID", "availability", "Disponibilidade e tenancy fora da matriz suportada.");
22
+ }
23
+ if (input.ecosystemRole && (input.ecosystemRole === "root") !== (availability === "system")) {
24
+ invalid("APP_ARCHITECTURE_INVALID", "ecosystemRole", "Somente a Central root usa availability=system.");
25
+ }
26
+ if (input.deploymentMode !== undefined && input.deploymentMode !== deploymentMode) {
27
+ invalid(input.deploymentMode === "hybrid" ? "APP_HYBRID_UNSUPPORTED" : "APP_DEPLOYMENT_CONTRADICTION", "deploymentMode", "Implantação legada deve concordar com tenancyModel; hybrid não é suportado.");
28
+ }
29
+ return Object.freeze({ availability, tenancyModel, deploymentMode });
30
+ }
31
+ module.exports = { DEPLOYMENT_BY_TENANCY, AVAILABILITY_TENANCY, deriveDeploymentMode, resolveAppArchitecture };
@@ -5,6 +5,7 @@ const path = require("node:path");
5
5
  const ctx = require("../core/context");
6
6
  const corePackage = require("../../package.json");
7
7
  const { validateRbac } = require("./rbacManifest");
8
+ const { deriveDeploymentMode, resolveAppArchitecture } = require("./appArchitecture");
8
9
 
9
10
  const APP_MANIFEST_FILENAME = "central.app.json";
10
11
  const APP_MANIFEST_SCHEMA_VERSION = 2;
@@ -13,10 +14,10 @@ const SUPPORTED_MODULES = Object.freeze([
13
14
  "collections", "documents", "pipelines", "assistants", "currencies",
14
15
  ]);
15
16
  const SUPPORTED_TENANCY_MODELS = Object.freeze(["none", "single_tenant", "multi_tenant"]);
16
- const SUPPORTED_AVAILABILITIES = Object.freeze(["system", "common", "licensed"]);
17
+ const SUPPORTED_AVAILABILITIES = Object.freeze(["system", "common", "self", "licensed"]);
17
18
  const SUPPORTED_ACCESS_MODES = Object.freeze(["tenant_members", "explicit_users", "authenticated_users"]);
18
19
  const SUPPORTED_AUTHORIZATION_MODES = Object.freeze(["none", "rbac"]);
19
- const SUPPORTED_DEPLOYMENT_MODES = Object.freeze(["singleton", "shared", "dedicated", "hybrid"]);
20
+ const SUPPORTED_DEPLOYMENT_MODES = Object.freeze(["singleton", "shared", "dedicated"]);
20
21
  const TRANSACTIONAL_EMAIL_CAPABILITY = "core.transactional-email";
21
22
  const EMAIL_SCOPE_POLICIES = Object.freeze(["app_only", "tenant_required", "tenant_override"]);
22
23
  const APP_KIND_ROLE = Object.freeze({
@@ -201,7 +202,7 @@ function dimensionsFor(manifest) {
201
202
  availability: manifest.availability || defaults.availability,
202
203
  accessMode: manifest.accessMode || defaults.accessMode,
203
204
  authorizationMode: manifest.authorizationMode || defaults.authorizationMode,
204
- deploymentMode: manifest.deploymentMode || defaults.deploymentMode,
205
+ deploymentMode: deriveDeploymentMode(SUPPORTED_TENANCY_MODELS.includes(manifest.tenancyModel) ? manifest.tenancyModel : defaults.tenancyModel),
205
206
  };
206
207
  }
207
208
 
@@ -238,6 +239,11 @@ function validateAppManifest(input, { coreVersion = corePackage.version } = {})
238
239
  validateDimension(input.deploymentMode, "deploymentMode", SUPPORTED_DEPLOYMENT_MODES, issues);
239
240
  const dimensions = dimensionsFor(input);
240
241
  const role = APP_KIND_ROLE[input.appKind];
242
+ try {
243
+ resolveAppArchitecture({ ...dimensions, ecosystemRole: role, ...(input.deploymentMode !== undefined ? { deploymentMode: input.deploymentMode } : {}) });
244
+ } catch (error) {
245
+ addIssue(issues, error.details?.field || "tenancyModel", `${error.code}: ${error.message}`);
246
+ }
241
247
  if (role === "root" && (
242
248
  dimensions.tenancyModel !== "none" ||
243
249
  dimensions.availability !== "system" ||