@oondemand/oon-core-back 0.6.13 → 0.7.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/bin/delivery.js +2 -5
- package/package.json +1 -1
- package/src/api/auth.routes.js +5 -1
- package/src/core/middlewares/auth.js +19 -0
- package/src/core/middlewares/rbac.js +6 -1
- package/src/core/utils/instanceActivation.js +20 -0
- package/src/delivery/mongoProvisioning.js +122 -117
- package/src/delivery/runtime.js +91 -166
- package/src/index.js +1 -7
- package/src/core/licensing.js +0 -71
- package/src/core/technicalIdentity.js +0 -174
- package/src/delivery/mongoCredentialLifecycle.js +0 -33
- package/src/delivery/preparedRuntime.js +0 -264
- package/src/delivery/preparedRuntime.md +0 -132
- package/src/delivery/technicalIdentity.js +0 -140
package/src/delivery/runtime.js
CHANGED
|
@@ -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 {
|
|
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, "
|
|
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 {
|
|
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: ${
|
|
43
|
+
name: ${context.namespace}
|
|
62
44
|
labels:
|
|
63
|
-
oon.dev/app-code: ${
|
|
64
|
-
oon.dev/environment: ${
|
|
65
|
-
app.kubernetes.io/managed-by: oon-platform
|
|
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:
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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(
|
|
107
|
-
const releaseAnnotation = JSON.stringify(String(
|
|
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: ${
|
|
112
|
-
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: ${
|
|
96
|
+
matchLabels: { app.kubernetes.io/name: ${context.appCode} }
|
|
117
97
|
template:
|
|
118
98
|
metadata:
|
|
119
99
|
labels:
|
|
120
|
-
app.kubernetes.io/name: ${
|
|
100
|
+
app.kubernetes.io/name: ${context.appCode}
|
|
121
101
|
app.kubernetes.io/part-of: oon-central
|
|
122
|
-
oon.dev/release-id: ${releaseLabel}
|
|
102
|
+
oon.dev/release-id: ${releaseLabel}
|
|
123
103
|
annotations:
|
|
124
104
|
oon.dev/release-id: ${releaseAnnotation}
|
|
125
105
|
spec:
|
|
126
|
-
serviceAccountName:
|
|
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:
|
|
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: ${
|
|
126
|
+
metadata: { name: ${context.appCode}, namespace: ${context.namespace} }
|
|
147
127
|
spec:
|
|
148
|
-
selector: { app.kubernetes.io/name: ${
|
|
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: ${
|
|
155
|
-
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: [${
|
|
139
|
+
tls: [{ hosts: [${context.hostname}], secretName: ${context.appCode}-tls }]
|
|
160
140
|
rules:
|
|
161
|
-
- host: ${
|
|
141
|
+
- host: ${context.hostname}
|
|
162
142
|
http:
|
|
163
143
|
paths:
|
|
164
144
|
- path: /
|
|
165
145
|
pathType: Prefix
|
|
166
146
|
backend:
|
|
167
|
-
service: { name: ${
|
|
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 = {}
|
|
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:
|
|
189
|
-
APP_CODE:
|
|
190
|
-
PUBLIC_APP_URL:
|
|
191
|
-
CENTRAL_ATIVACAO_URL:
|
|
192
|
-
CENTRAL_ATIVACAO_API_URL:
|
|
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
|
-
...
|
|
195
|
-
? {
|
|
196
|
-
OON_INSTANCE_UID: identity.instanceUid,
|
|
197
|
-
OON_TECHNICAL_IDENTITY_VERSION: String(identity.version),
|
|
198
|
-
}
|
|
199
|
-
: {}),
|
|
171
|
+
...extraSecrets,
|
|
200
172
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
243
|
-
let
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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 (!
|
|
335
|
-
const response = await fetch(`${
|
|
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":
|
|
278
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": context.releaseId },
|
|
338
279
|
body: JSON.stringify({
|
|
339
|
-
codigoAtivacao:
|
|
340
|
-
nomeInstancia: `${
|
|
280
|
+
codigoAtivacao: context.activationCode,
|
|
281
|
+
nomeInstancia: `${context.appCode} dev`,
|
|
341
282
|
parametrosGlobais: {
|
|
342
|
-
nomeInstancia: `${
|
|
343
|
-
urlPublica:
|
|
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
|
+
};
|
package/src/core/licensing.js
DELETED
|
@@ -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
|
-
});
|