@oondemand/oon-core-back 0.6.13 → 0.7.1

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,9 +4,7 @@ const crypto = require("node:crypto");
4
4
  const { spawnSync } = require("node:child_process");
5
5
  const { kubectl } = require("./command");
6
6
  const { validateInfraFromCluster } = require("../deploy/contracts/infra-contract");
7
- const { resolveMongoCredential } = require("./mongoCredentialLifecycle");
8
- const { provisionMongo, validateMongoUri, mongoIdentity, recoveryRequired } = require("./mongoProvisioning");
9
- const { canonicalizeDeliveryContext } = require("./technicalIdentity");
7
+ const { provisionMongo, validateMongoUri } = require("./mongoProvisioning");
10
8
 
11
9
  const RESOURCE_PROFILES = {
12
10
  nano: { cpuRequest: "50m", cpuLimit: "250m", memoryRequest: "96Mi", memoryLimit: "256Mi" },
@@ -33,45 +31,29 @@ function applyYaml(yaml) {
33
31
  }
34
32
  function readSecret(namespace, name, key) {
35
33
  try {
36
- const value = kubectl(["-n", namespace, "get", "secret", name, "--ignore-not-found", "-o", `jsonpath={.data.${key}}`], { capture: true });
34
+ const value = kubectl(["-n", namespace, "get", "secret", name, "-o", `jsonpath={.data.${key}}`], { capture: true });
37
35
  return value ? Buffer.from(value, "base64").toString("utf8") : null;
38
- } catch { throw recoveryRequired("MONGO_SECRET_READ_FAILED"); }
39
- }
40
-
41
- function canonicalRuntimeContext(context) {
42
- return canonicalizeDeliveryContext(context);
43
- }
44
-
45
- function identityLabels(identity) {
46
- if (identity.mode !== "canonical") return "";
47
- return `\n oon.runtime/instance-uid: ${identity.instanceUid}\n oon.runtime/identity-version: \"${identity.version}\"`;
48
- }
49
-
50
- function podIdentityLabels(identity) {
51
- if (identity.mode !== "canonical") return "";
52
- return `\n oon.runtime/instance-uid: ${identity.instanceUid}\n oon.runtime/identity-version: \"${identity.version}\"`;
36
+ } catch { return null; }
53
37
  }
54
38
 
55
39
  function baselineManifest({ context }) {
56
- const normalized = canonicalRuntimeContext(context);
57
- const identity = normalized.deliveryTechnicalIdentity;
58
40
  return `apiVersion: v1
59
41
  kind: Namespace
60
42
  metadata:
61
- name: ${normalized.namespace}
43
+ name: ${context.namespace}
62
44
  labels:
63
- oon.dev/app-code: ${normalized.appCode}
64
- oon.dev/environment: ${normalized.environment}
65
- app.kubernetes.io/managed-by: oon-platform${identityLabels(identity)}
45
+ oon.dev/app-code: ${context.appCode}
46
+ oon.dev/environment: ${context.environment}
47
+ app.kubernetes.io/managed-by: oon-platform
66
48
  ---
67
49
  apiVersion: v1
68
50
  kind: ServiceAccount
69
- metadata: { name: ${identity.serviceAccount}, namespace: ${normalized.namespace} }
51
+ metadata: { name: oon-runtime, namespace: ${context.namespace} }
70
52
  automountServiceAccountToken: false
71
53
  ---
72
54
  apiVersion: v1
73
55
  kind: LimitRange
74
- metadata: { name: oon-runtime-defaults, namespace: ${normalized.namespace} }
56
+ metadata: { name: oon-runtime-defaults, namespace: ${context.namespace} }
75
57
  spec:
76
58
  limits:
77
59
  - type: Container
@@ -80,13 +62,13 @@ spec:
80
62
  ---
81
63
  apiVersion: v1
82
64
  kind: ResourceQuota
83
- metadata: { name: oon-runtime-quota, namespace: ${normalized.namespace} }
65
+ metadata: { name: oon-runtime-quota, namespace: ${context.namespace} }
84
66
  spec:
85
67
  hard: { requests.cpu: "2", requests.memory: 2Gi, limits.cpu: "4", limits.memory: 4Gi, pods: "20", services: "10" }
86
68
  ---
87
69
  apiVersion: networking.k8s.io/v1
88
70
  kind: NetworkPolicy
89
- metadata: { name: oon-runtime-baseline, namespace: ${normalized.namespace} }
71
+ metadata: { name: oon-runtime-baseline, namespace: ${context.namespace} }
90
72
  spec:
91
73
  podSelector: {}
92
74
  policyTypes: [Ingress, Egress]
@@ -100,37 +82,35 @@ spec:
100
82
  }
101
83
 
102
84
  function runtimeManifest({ context, infra, imageRef, profile = "small" }) {
103
- const normalized = canonicalRuntimeContext(context);
104
- const identity = normalized.deliveryTechnicalIdentity;
105
85
  const resources = RESOURCE_PROFILES[profile] || RESOURCE_PROFILES.small;
106
- const releaseLabel = kubernetesLabelValue(normalized.releaseId);
107
- const releaseAnnotation = JSON.stringify(String(normalized.releaseId));
86
+ const releaseLabel = kubernetesLabelValue(context.releaseId);
87
+ const releaseAnnotation = JSON.stringify(String(context.releaseId));
108
88
  return `apiVersion: apps/v1
109
89
  kind: Deployment
110
90
  metadata:
111
- name: ${normalized.appCode}
112
- namespace: ${normalized.namespace}
91
+ name: ${context.appCode}
92
+ namespace: ${context.namespace}
113
93
  spec:
114
94
  replicas: 1
115
95
  selector:
116
- matchLabels: { app.kubernetes.io/name: ${normalized.appCode} }
96
+ matchLabels: { app.kubernetes.io/name: ${context.appCode} }
117
97
  template:
118
98
  metadata:
119
99
  labels:
120
- app.kubernetes.io/name: ${normalized.appCode}
100
+ app.kubernetes.io/name: ${context.appCode}
121
101
  app.kubernetes.io/part-of: oon-central
122
- oon.dev/release-id: ${releaseLabel}${podIdentityLabels(identity)}
102
+ oon.dev/release-id: ${releaseLabel}
123
103
  annotations:
124
104
  oon.dev/release-id: ${releaseAnnotation}
125
105
  spec:
126
- serviceAccountName: ${identity.serviceAccount}
106
+ serviceAccountName: oon-runtime
127
107
  imagePullSecrets: [{ name: ${infra.REGISTRY_PULL_SECRET_NAME || "ghcr-oon"} }]
128
108
  containers:
129
109
  - name: central
130
110
  image: ${imageRef}
131
111
  imagePullPolicy: IfNotPresent
132
112
  ports: [{ name: http, containerPort: 8080 }]
133
- envFrom: [{ secretRef: { name: ${identity.secret} } }]
113
+ envFrom: [{ secretRef: { name: oon-runtime } }]
134
114
  resources:
135
115
  requests: { cpu: ${resources.cpuRequest}, memory: ${resources.memoryRequest} }
136
116
  limits: { cpu: ${resources.cpuLimit}, memory: ${resources.memoryLimit} }
@@ -143,28 +123,28 @@ spec:
143
123
  ---
144
124
  apiVersion: v1
145
125
  kind: Service
146
- metadata: { name: ${normalized.appCode}, namespace: ${normalized.namespace} }
126
+ metadata: { name: ${context.appCode}, namespace: ${context.namespace} }
147
127
  spec:
148
- selector: { app.kubernetes.io/name: ${normalized.appCode} }
128
+ selector: { app.kubernetes.io/name: ${context.appCode} }
149
129
  ports: [{ name: http, port: 80, targetPort: http }]
150
130
  ---
151
131
  apiVersion: networking.k8s.io/v1
152
132
  kind: Ingress
153
133
  metadata:
154
- name: ${normalized.appCode}
155
- namespace: ${normalized.namespace}
134
+ name: ${context.appCode}
135
+ namespace: ${context.namespace}
156
136
  annotations: { cert-manager.io/cluster-issuer: ${infra.CLUSTER_ISSUER} }
157
137
  spec:
158
138
  ingressClassName: ${infra.INGRESS_CLASS}
159
- tls: [{ hosts: [${normalized.hostname}], secretName: ${identity.tlsSecret} }]
139
+ tls: [{ hosts: [${context.hostname}], secretName: ${context.appCode}-tls }]
160
140
  rules:
161
- - host: ${normalized.hostname}
141
+ - host: ${context.hostname}
162
142
  http:
163
143
  paths:
164
144
  - path: /
165
145
  pathType: Prefix
166
146
  backend:
167
- service: { name: ${normalized.appCode}, port: { name: http } }
147
+ service: { name: ${context.appCode}, port: { name: http } }
168
148
  `;
169
149
  }
170
150
 
@@ -178,41 +158,27 @@ function copyPullSecret(namespace, infra) {
178
158
  } catch (error) { if (!/not found/i.test(error.message)) throw error; }
179
159
  }
180
160
 
181
- function applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets = {}, record, writeRecord }) {
182
- const normalized = canonicalRuntimeContext(context);
183
- const identity = normalized.deliveryTechnicalIdentity;
161
+ function applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets = {} }) {
184
162
  const literals = {
185
- ...extraSecrets,
186
163
  MONGO_URI: mongoUri,
187
164
  NODE_ENV: "production",
188
- APP_ENVIRONMENT: normalized.environment,
189
- APP_CODE: normalized.appCode,
190
- PUBLIC_APP_URL: normalized.publicUrl,
191
- CENTRAL_ATIVACAO_URL: normalized.activationApiUrl,
192
- CENTRAL_ATIVACAO_API_URL: normalized.activationApiUrl,
165
+ APP_ENVIRONMENT: context.environment,
166
+ APP_CODE: context.appCode,
167
+ PUBLIC_APP_URL: context.publicUrl,
168
+ CENTRAL_ATIVACAO_URL: context.activationApiUrl,
169
+ CENTRAL_ATIVACAO_API_URL: context.activationApiUrl,
193
170
  INSTANCE_CREDENTIAL_ENCRYPTION_KEY: encryptionKey,
194
- ...(identity.mode === "canonical"
195
- ? {
196
- OON_INSTANCE_UID: identity.instanceUid,
197
- OON_TECHNICAL_IDENTITY_VERSION: String(identity.version),
198
- }
199
- : {}),
171
+ ...extraSecrets,
200
172
  };
201
- writeRecord("replace", {
202
- ...record,
203
- metadata: { ...record.metadata, annotations: {
204
- ...record.metadata.annotations, "oon.runtime/mongo-state": "ready",
205
- } },
206
- data: { ...record.data, ...Object.fromEntries(Object.entries(literals)
207
- .map(([key, value]) => [key, Buffer.from(String(value)).toString("base64")])) },
208
- });
173
+ const args = ["-n", context.namespace, "create", "secret", "generic", "oon-runtime"];
174
+ for (const [key, value] of Object.entries(literals)) args.push(`--from-literal=${key}=${value}`);
175
+ args.push("--dry-run=client", "-o", "yaml");
176
+ applyYaml(kubectl(args, { capture: true }));
209
177
  }
210
178
 
211
179
  async function provisionRuntime({ context, extraSecrets = {} }) {
212
- const normalized = canonicalRuntimeContext(context);
213
- const identity = normalized.deliveryTechnicalIdentity;
214
180
  const infra = validateInfraFromCluster().config;
215
- applyYaml(baselineManifest({ context: normalized, infra }));
181
+ applyYaml(baselineManifest({ context, infra }));
216
182
 
217
183
  const platformNamespace = infra.PLATFORM_NAMESPACE || "oon-system";
218
184
  const adminSecretName = infra.MONGO_ADMIN_SECRET_NAME || "oon-mongo-admin";
@@ -239,77 +205,53 @@ async function provisionRuntime({ context, extraSecrets = {} }) {
239
205
  "DIGITALOCEAN_DATABASE_CLUSTER_ID",
240
206
  );
241
207
 
242
- // Read errors are distinct from confirmed absence. Never provision on a failed GET.
243
- let record;
244
- try {
245
- const raw = kubectl(["-n", normalized.namespace, "get", "secret", identity.secret,
246
- "--ignore-not-found", "-o", "json"], { capture: true });
247
- record = raw ? JSON.parse(raw) : null;
248
- } catch { throw recoveryRequired("MONGO_SECRET_READ_FAILED"); }
249
- const decode = (key) => record?.data?.[key]
250
- ? Buffer.from(record.data[key], "base64").toString("utf8") : null;
251
- const encryptionKey = record === null ? randomSecret(48) : decode("INSTANCE_CREDENTIAL_ENCRYPTION_KEY");
252
- if (!encryptionKey) throw recoveryRequired("MONGO_ENCRYPTION_KEY_MISSING");
253
- const writeRecord = (operation, value) => {
254
- // Secrets travel through stdin and captured output, never inherited stderr/argv.
255
- const result = spawnSync("kubectl", [operation, "-f", "-", "-o", "json"], {
256
- input: JSON.stringify(value), encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
208
+ let mongoUri = readSecret(context.namespace, "oon-runtime", "MONGO_URI");
209
+ let databaseName = "";
210
+ let credentialRotated = false;
211
+
212
+ if (mongoUri) {
213
+ try {
214
+ await validateMongoUri(mongoUri);
215
+ } catch (error) {
216
+ console.warn(
217
+ "A credencial MongoDB persistida não passou na validação; " +
218
+ "ela será reprovisionada.",
219
+ );
220
+ mongoUri = null;
221
+ credentialRotated = true;
222
+ }
223
+ }
224
+
225
+ if (!mongoUri) {
226
+ const provisioned = await provisionMongo({
227
+ adminUri,
228
+ appCode: context.appCode,
229
+ environment: context.environment,
230
+ provisioner: mongoProvisioner,
231
+ digitalOceanToken,
232
+ digitalOceanClusterId,
257
233
  });
258
- if (result.status !== 0) throw recoveryRequired("MONGO_SECRET_WRITE_FAILED");
259
- try { record = JSON.parse(result.stdout); }
260
- catch { throw recoveryRequired("MONGO_SECRET_RESPONSE_INVALID"); }
261
- if (!record?.metadata?.resourceVersion) throw recoveryRequired("MONGO_SECRET_RESPONSE_INVALID");
262
- };
263
- const mongo = await resolveMongoCredential({
264
- identity: mongoIdentity(normalized.appCode, normalized.environment,
265
- identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}),
266
- read: async () => record === null ? null : { mongoUri: decode("MONGO_URI") },
267
- begin: async () => writeRecord("create", {
268
- apiVersion: "v1", kind: "Secret", type: "Opaque",
269
- metadata: { name: identity.secret, namespace: normalized.namespace,
270
- annotations: { "oon.runtime/mongo-state": "provisioning" } },
271
- data: { INSTANCE_CREDENTIAL_ENCRYPTION_KEY: Buffer.from(encryptionKey).toString("base64") },
272
- }),
273
- persist: async (uri) => writeRecord("replace", {
274
- ...record,
275
- metadata: { ...record.metadata, annotations: {
276
- ...record.metadata.annotations, "oon.runtime/mongo-state": "credential-captured",
277
- } },
278
- data: { ...record.data, MONGO_URI: Buffer.from(uri).toString("base64") },
279
- }),
280
- provision: (persistCredential) => provisionMongo({
281
- adminUri, appCode: normalized.appCode, environment: normalized.environment,
282
- ...(identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}),
283
- provisioner: mongoProvisioner, digitalOceanToken, digitalOceanClusterId,
284
- }, { persistCredential }),
285
- validate: validateMongoUri,
286
- });
287
- const mongoUri = mongo.mongoUri;
288
- const databaseName = mongoIdentity(normalized.appCode, normalized.environment,
289
- identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}).dbName;
290
- const credentialRotated = false;
291
- applyRuntimeSecret({ context: normalized, mongoUri, encryptionKey, extraSecrets, record, writeRecord });
292
- return {
293
- infra,
294
- databaseName,
295
- credentialRotated,
296
- ...(identity.mode === "canonical" ? { technicalIdentity: identity } : {}),
297
- };
234
+ mongoUri = provisioned.mongoUri;
235
+ databaseName = provisioned.dbName;
236
+ credentialRotated = true;
237
+ }
238
+
239
+ const encryptionKey =
240
+ readSecret(
241
+ context.namespace,
242
+ "oon-runtime",
243
+ "INSTANCE_CREDENTIAL_ENCRYPTION_KEY",
244
+ ) || randomSecret(48);
245
+ applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets });
246
+ return { infra, databaseName, credentialRotated };
298
247
  }
299
248
 
300
249
  function deployRuntime({ context, infra, imageDigest, resourceProfile = "small" }) {
301
- const normalized = canonicalRuntimeContext(context);
302
- copyPullSecret(normalized.namespace, infra);
303
- const imageRef = `${normalized.imageRepository}@${imageDigest}`;
304
- applyYaml(runtimeManifest({ context: normalized, infra, imageRef, profile: resourceProfile }));
305
- kubectl(["-n", normalized.namespace, "rollout", "status", `deployment/${normalized.appCode}`, "--timeout=10m"]);
306
- return {
307
- infraVersion: infra.INFRA_SCHEMA_VERSION,
308
- imageRef,
309
- ...(normalized.deliveryTechnicalIdentity.mode === "canonical"
310
- ? { technicalIdentity: normalized.deliveryTechnicalIdentity }
311
- : {}),
312
- };
250
+ copyPullSecret(context.namespace, infra);
251
+ const imageRef = `${context.imageRepository}@${imageDigest}`;
252
+ applyYaml(runtimeManifest({ context, infra, imageRef, profile: resourceProfile }));
253
+ kubectl(["-n", context.namespace, "rollout", "status", `deployment/${context.appCode}`, "--timeout=10m"]);
254
+ return { infraVersion: infra.INFRA_SCHEMA_VERSION, imageRef };
313
255
  }
314
256
 
315
257
  async function waitHttp(url, { timeoutMs = 300000, expectedStatus = 200 } = {}) {
@@ -327,20 +269,19 @@ async function waitHttp(url, { timeoutMs = 300000, expectedStatus = 200 } = {})
327
269
  }
328
270
 
329
271
  async function activateMember(context) {
330
- const normalized = canonicalRuntimeContext(context);
331
- const statusResponse = await waitHttp(`${normalized.publicUrl}/api/ativacao/status`);
272
+ const statusResponse = await waitHttp(`${context.publicUrl}/api/ativacao/status`);
332
273
  const status = await statusResponse.json();
333
274
  if (status.status === "ativa") return { alreadyActive: true, instanceId: status.instanceId };
334
- if (!normalized.activationCode) throw new Error("Instância não está ativa e não há código de ativação autorizado.");
335
- const response = await fetch(`${normalized.publicUrl}/api/ativacao/concluir`, {
275
+ if (!context.activationCode) throw new Error("Instância não está ativa e não há código de ativação autorizado.");
276
+ const response = await fetch(`${context.publicUrl}/api/ativacao/concluir`, {
336
277
  method: "POST",
337
- headers: { "Content-Type": "application/json", "Idempotency-Key": normalized.releaseId },
278
+ headers: { "Content-Type": "application/json", "Idempotency-Key": context.releaseId },
338
279
  body: JSON.stringify({
339
- codigoAtivacao: normalized.activationCode,
340
- nomeInstancia: `${normalized.appCode} dev`,
280
+ codigoAtivacao: context.activationCode,
281
+ nomeInstancia: `${context.appCode} dev`,
341
282
  parametrosGlobais: {
342
- nomeInstancia: `${normalized.appCode} dev`, ambiente: normalized.environment,
343
- urlPublica: normalized.publicUrl, emailAdministrativo: normalized.administrativeEmail,
283
+ nomeInstancia: `${context.appCode} dev`, ambiente: context.environment,
284
+ urlPublica: context.publicUrl, emailAdministrativo: context.administrativeEmail,
344
285
  idioma: "pt-BR", timezone: "America/Sao_Paulo", moeda: "BRL", pais: "BR", formatoData: "DD/MM/YYYY",
345
286
  },
346
287
  parametrosAplicacao: {},
@@ -351,20 +292,4 @@ async function activateMember(context) {
351
292
  return data;
352
293
  }
353
294
 
354
- module.exports = {
355
- RESOURCE_PROFILES,
356
- randomSecret,
357
- dnsLabel,
358
- kubernetesLabelValue,
359
- canonicalRuntimeContext,
360
- provisionMongo,
361
- validateMongoUri,
362
- baselineManifest,
363
- runtimeManifest,
364
- provisionRuntime,
365
- deployRuntime,
366
- // Explicit opt-in for a separately prepared target; never a provisioning fallback.
367
- deployPreparedRuntime: require("./preparedRuntime").deployPreparedRuntime,
368
- waitHttp,
369
- activateMember,
370
- };
295
+ module.exports = { RESOURCE_PROFILES, randomSecret, dnsLabel, kubernetesLabelValue, provisionMongo, validateMongoUri, baselineManifest, runtimeManifest, provisionRuntime, deployRuntime, waitHttp, activateMember };
package/src/index.js CHANGED
@@ -10,7 +10,6 @@ const tenancy = require("./core/tenancy");
10
10
  const GenericError = require("./core/errors/GenericError");
11
11
  const { requirePermission } = require("./core/middlewares/permissions");
12
12
  const rbacPolicy = require("./core/rbac/policy");
13
- const technicalIdentity = require("./core/technicalIdentity");
14
13
  const capabilities = require("./capabilities");
15
14
  const { operationalRequestHeaders } = require("./core/utils/instanceActivation");
16
15
  const appManifest = require("./manifest/appManifest");
@@ -76,15 +75,10 @@ module.exports = {
76
75
  drainProcessJobs: processRuntime.drainProcessJobs,
77
76
  fields,
78
77
  capabilities,
79
- technicalIdentity,
80
78
  operationalRequestHeaders,
81
- licensing: {
82
- assertBusinessOperation: require("./core/licensing").assertBusinessOperation,
83
- verifyConsumerRequest: require("./core/licensing").verifyConsumerRequest,
84
- },
85
79
  localDevelopment,
86
80
  localExecutionPolicy,
87
81
  PdfRenderingError: capabilities.PdfRenderingError,
88
82
  TransactionalEmailError: capabilities.TransactionalEmailError,
89
83
  GenericError, registry,
90
- };
84
+ };
@@ -1,71 +0,0 @@
1
- "use strict";
2
-
3
- const axios = require("axios");
4
- const GenericError = require("./errors/GenericError");
5
- const { operationalRequestHeaders } = require("./utils/instanceActivation");
6
- const { getBaseUrl, getAppCode } = require("./utils/activationProvider");
7
- const { isLocalRuntime } = require("./runtime/localDevelopment");
8
-
9
- const BLOCKING_CODES = new Set(["APP_LICENSE_REQUIRED", "APP_LICENSE_NOT_STARTED", "APP_LICENSE_EXPIRED", "APP_LICENSE_SUSPENDED", "APP_LICENSE_CANCELLED", "APP_LICENSE_OVERDUE", "APP_LICENSE_INVALID", "LICENSE_REQUIRED", "LICENSE_INACTIVE", "LICENSE_EXPIRED", "LICENSE_NOT_STARTED", "APP_ENTITLEMENT_INACTIVE", "APP_ENTITLEMENT_REQUIRED", "APP_ENTITLEMENT_OUT_OF_TERM"]);
10
- const TIMEOUT_MS = 3000;
11
-
12
- function unavailable() {
13
- return new GenericError("Não foi possível consultar a licença. A operação permanece pendente.", { statusCode: 503, code: "LICENSE_AUTHORITY_UNAVAILABLE" });
14
- }
15
- function blocked(code) {
16
- return new GenericError("A licença não permite iniciar esta operação. Consulte o responsável pelo contrato.", { statusCode: 423, code });
17
- }
18
-
19
- // No positive cache and no retries. Call immediately before each new business effect.
20
- async function assertBusinessOperation({ tenantId, correlationId } = {}, dependencies = {}) {
21
- if (!tenantId) throw new GenericError("Contexto de tenant obrigatório.", { statusCode: 403, code: "TENANT_CONTEXT_REQUIRED" });
22
- const headers = await (dependencies.operationalRequestHeaders || operationalRequestHeaders)({ tenantId, correlationId });
23
- const appCode = (dependencies.getAppCode || getAppCode)();
24
- const baseUrl = (dependencies.getBaseUrl || getBaseUrl)();
25
- let response;
26
- try {
27
- response = await (dependencies.post || axios.post)(`${baseUrl}/licensing/decision`, {}, {
28
- timeout: TIMEOUT_MS, maxRedirects: 0, maxContentLength: 16384, headers,
29
- });
30
- } catch (error) {
31
- const code = error.response?.data?.error?.code || error.response?.data?.code;
32
- if (BLOCKING_CODES.has(code) && [402, 403, 409, 423].includes(error.response?.status)) throw blocked(code);
33
- throw unavailable();
34
- }
35
- const result = response?.data;
36
- if (result?.schemaVersion !== 1 || result.tenantId !== String(tenantId) || result.appCode !== appCode
37
- || !["desenvolvimento", "homologacao", "producao"].includes(result.environment)
38
- || result.maxAgeSeconds !== 0 || typeof result.allowed !== "boolean") throw unavailable();
39
- if (!result.allowed) {
40
- if (BLOCKING_CODES.has(result.code)) throw blocked(result.code);
41
- throw unavailable();
42
- }
43
- if (result.code != null || !Number.isSafeInteger(result.version) || result.version < 1) throw unavailable();
44
- return Object.freeze({ ...result });
45
- }
46
-
47
- // An integration service authenticates the originating Deployment at the authority.
48
- // Forward only this allowlist, never the gateway bearer or arbitrary request headers.
49
- async function verifyConsumerRequest({ headers: input = {}, requireLicense = true } = {}, dependencies = {}) {
50
- if (isLocalRuntime()) throw new GenericError("Identidade operacional indisponível localmente.", { statusCode: 403, code: "LOCAL_OPERATION_NOT_SUPPORTED" });
51
- const headers = {};
52
- for (const key of ["x-oon-deployment-id", "x-oon-deployment-token", "x-app-code", "x-oon-tenant-id"]) {
53
- if (typeof input[key] !== "string" || !input[key] || input[key].length > 512) throw new GenericError("Credencial operacional inválida.", { statusCode: 401, code: "OPERATIONAL_CREDENTIAL_REQUIRED" });
54
- headers[key] = input[key];
55
- }
56
- if (requireLicense) {
57
- return assertBusinessOperation({ tenantId: headers["x-oon-tenant-id"] }, { ...dependencies,
58
- getAppCode: () => headers["x-app-code"], operationalRequestHeaders: async () => headers });
59
- }
60
- try {
61
- const response = await (dependencies.post || axios.post)(`${(dependencies.getBaseUrl || getBaseUrl)()}/licensing/identity`, {}, { timeout: TIMEOUT_MS, maxRedirects: 0, maxContentLength: 16384, headers });
62
- const result = response?.data;
63
- if (result?.schemaVersion !== 1 || result.appCode !== headers["x-app-code"] || result.tenantId !== headers["x-oon-tenant-id"] || !["desenvolvimento", "homologacao", "producao"].includes(result.environment) || result.maxAgeSeconds !== 0) throw unavailable();
64
- return Object.freeze({ appCode: result.appCode, tenantId: result.tenantId, environment: result.environment });
65
- } catch (error) {
66
- if ([401, 403].includes(error.response?.status)) throw new GenericError("Credencial operacional inválida.", { statusCode: 401, code: "OPERATIONAL_CREDENTIAL_INVALID" });
67
- throw unavailable();
68
- }
69
- }
70
-
71
- module.exports = { assertBusinessOperation, verifyConsumerRequest, BLOCKING_CODES, TIMEOUT_MS };
@@ -1,174 +0,0 @@
1
- "use strict";
2
-
3
- const crypto = require("node:crypto");
4
-
5
- const APP_CODE_MAX_LENGTH = 24;
6
- const APP_CODE_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,22}[a-z0-9])?$/;
7
- const INSTANCE_UID_ALPHABET = "23456789abcdefghjkmnopqrstuvwxyz";
8
- const INSTANCE_UID_LENGTH = 16;
9
- const INSTANCE_UID_BYTES = 10;
10
- const INSTANCE_UID_PATTERN = /^[23456789abcdefghjkmnopqrstuvwxyz]{16}$/;
11
-
12
- const ENVIRONMENT_TOKENS = Object.freeze({
13
- desenvolvimento: "dev",
14
- homologacao: "hml",
15
- producao: "prod",
16
- dev: "dev",
17
- hml: "hml",
18
- prod: "prod",
19
- });
20
-
21
- class TechnicalIdentityError extends Error {
22
- constructor(message, code, details = {}) {
23
- super(message);
24
- this.name = "TechnicalIdentityError";
25
- this.code = code;
26
- this.details = details;
27
- }
28
- }
29
-
30
- function assertAppCode(value) {
31
- const appCode = typeof value === "string" ? value : "";
32
- if (!APP_CODE_PATTERN.test(appCode)) {
33
- throw new TechnicalIdentityError(
34
- `appCode deve seguir ${APP_CODE_PATTERN} e possuir no máximo ${APP_CODE_MAX_LENGTH} caracteres ASCII.`,
35
- "TECHNICAL_APP_CODE_INVALID",
36
- { appCode },
37
- );
38
- }
39
- return appCode;
40
- }
41
-
42
- function assertInstanceUid(value) {
43
- const instanceUid = typeof value === "string" ? value : "";
44
- if (!INSTANCE_UID_PATTERN.test(instanceUid)) {
45
- throw new TechnicalIdentityError(
46
- `instanceUid deve possuir ${INSTANCE_UID_LENGTH} caracteres no alfabeto canônico.`,
47
- "TECHNICAL_INSTANCE_UID_INVALID",
48
- { instanceUid },
49
- );
50
- }
51
- return instanceUid;
52
- }
53
-
54
- function environmentToken(value) {
55
- const environment = String(value || "").trim().toLowerCase();
56
- const hasEnvironment = Object.prototype.hasOwnProperty.call(ENVIRONMENT_TOKENS, environment);
57
- const token = hasEnvironment ? ENVIRONMENT_TOKENS[environment] : null;
58
- if (!token) {
59
- throw new TechnicalIdentityError(
60
- "Ambiente técnico inválido.",
61
- "TECHNICAL_ENVIRONMENT_INVALID",
62
- { environment },
63
- );
64
- }
65
- return token;
66
- }
67
-
68
- function encodeInstanceUid(bytes) {
69
- const buffer = Buffer.from(bytes || []);
70
- if (buffer.length !== INSTANCE_UID_BYTES) {
71
- throw new TechnicalIdentityError(
72
- `instanceUid exige exatamente ${INSTANCE_UID_BYTES} bytes aleatórios.`,
73
- "TECHNICAL_INSTANCE_UID_RANDOM_BYTES_INVALID",
74
- { receivedBytes: buffer.length },
75
- );
76
- }
77
-
78
- let accumulator = 0;
79
- let bits = 0;
80
- let output = "";
81
- for (const byte of buffer) {
82
- accumulator = (accumulator << 8) | byte;
83
- bits += 8;
84
- while (bits >= 5) {
85
- bits -= 5;
86
- output += INSTANCE_UID_ALPHABET[(accumulator >> bits) & 31];
87
- accumulator &= (1 << bits) - 1;
88
- }
89
- }
90
-
91
- if (bits !== 0 || output.length !== INSTANCE_UID_LENGTH) {
92
- throw new TechnicalIdentityError(
93
- "Falha ao codificar instanceUid de 80 bits.",
94
- "TECHNICAL_INSTANCE_UID_ENCODING_INVALID",
95
- );
96
- }
97
- return output;
98
- }
99
-
100
- function generateInstanceUid(randomBytes = crypto.randomBytes) {
101
- if (typeof randomBytes !== "function") {
102
- throw new TypeError("randomBytes deve ser uma função.");
103
- }
104
- return encodeInstanceUid(randomBytes(INSTANCE_UID_BYTES));
105
- }
106
-
107
- function technicalIdentity({ appCode, instanceUid, environment }) {
108
- const canonicalAppCode = assertAppCode(appCode);
109
- const canonicalInstanceUid = assertInstanceUid(instanceUid);
110
- const env = environmentToken(environment);
111
- const base = `${canonicalAppCode}-${canonicalInstanceUid}-${env}`;
112
- const database = `${canonicalAppCode}_${canonicalInstanceUid}_${env}`;
113
- const mongoUsername = `${base}-rw`;
114
- const serviceAccount = `${base}-runtime`;
115
- const secret = `${base}-runtime`;
116
- const provisionJob = `${base}-provision`;
117
- const frontendDnsLabel = base;
118
- const backendDnsLabel = `${base}-api`;
119
- const tlsSecret = `${base}-tls`;
120
-
121
- const lengths = {
122
- base: base.length,
123
- database: database.length,
124
- mongoUsername: mongoUsername.length,
125
- serviceAccount: serviceAccount.length,
126
- secret: secret.length,
127
- provisionJob: provisionJob.length,
128
- frontendDnsLabel: frontendDnsLabel.length,
129
- backendDnsLabel: backendDnsLabel.length,
130
- tlsSecret: tlsSecret.length,
131
- };
132
- const invalidLength = Object.entries(lengths).find(([, length]) => length > 63);
133
- if (invalidLength) {
134
- throw new TechnicalIdentityError(
135
- "Identidade técnica excede o orçamento DNS/Kubernetes de 63 caracteres.",
136
- "TECHNICAL_IDENTITY_BUDGET_EXCEEDED",
137
- { resource: invalidLength[0], length: invalidLength[1] },
138
- );
139
- }
140
-
141
- return Object.freeze({
142
- version: 1,
143
- appCode: canonicalAppCode,
144
- instanceUid: canonicalInstanceUid,
145
- environment: env,
146
- resourceBase: base,
147
- namespace: base,
148
- database,
149
- mongoUsername,
150
- serviceAccount,
151
- secret,
152
- provisionJob,
153
- frontendDnsLabel,
154
- backendDnsLabel,
155
- tlsSecret,
156
- });
157
- }
158
-
159
- module.exports = Object.freeze({
160
- APP_CODE_MAX_LENGTH,
161
- APP_CODE_PATTERN,
162
- INSTANCE_UID_ALPHABET,
163
- INSTANCE_UID_LENGTH,
164
- INSTANCE_UID_BYTES,
165
- INSTANCE_UID_PATTERN,
166
- ENVIRONMENT_TOKENS,
167
- TechnicalIdentityError,
168
- assertAppCode,
169
- assertInstanceUid,
170
- environmentToken,
171
- encodeInstanceUid,
172
- generateInstanceUid,
173
- technicalIdentity,
174
- });