@oondemand/oon-core-back 0.4.23 → 0.4.25

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
@@ -15,6 +15,13 @@ O Core suporta `ecosystem.role` em `central.config.js`: `root` para a Central de
15
15
 
16
16
  Compatibilidade temporária: `CENTRAL_ATIVACAO_BACKEND_URL` e `MEUS_APPS_BACKEND_URL` continuam aceitas como aliases da URL da API. Nunca aponte a URL da API para o próprio Core.
17
17
 
18
+ ## Saúde operacional
19
+
20
+ - `GET /health/ready`: retorna HTTP 200 somente quando o MongoDB está conectado; retorna 503 enquanto o runtime não estiver pronto.
21
+ - `GET /health/version`: expõe as versões do OonCore e da Central, commit, release, build e ambiente da publicação.
22
+
23
+ Na imagem de entrega, essas rotas são publicadas pelo Nginx sob `/api/health/ready` e `/api/health/version`.
24
+
18
25
  ## Fluxo
19
26
 
20
27
  `GET /ativacao/status` informa estado sem segredos. `POST /ativacao/validar-codigo` valida sem consumir. `POST /ativacao/concluir` revalida, chama `/ativar`, criptografa imediatamente o token da instância, executa hooks declarativos, chama `/concluir` e marca a instância como `ativa`. `POST /ativacao/tentar-novamente` retoma uma ativação já registrada sem exigir novo código.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.4.23",
3
+ "version": "0.4.25",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -1,21 +1,65 @@
1
+ "use strict";
2
+
1
3
  const express = require("express");
2
4
  const mongoose = require("mongoose");
3
5
  const ctx = require("../core/context");
6
+ const { runtimeSummary } = require("./core.routes");
4
7
 
5
8
  const router = express.Router();
6
9
 
7
- router.get("/", (_req, res) => {
8
- const states = {
9
- 0: "Desconectado",
10
- 1: "Conectado",
11
- 2: "Conectando",
12
- 3: "Desconectando",
10
+ const DATABASE_STATES = Object.freeze({
11
+ 0: { code: "disconnected", label: "Desconectado" },
12
+ 1: { code: "connected", label: "Conectado" },
13
+ 2: { code: "connecting", label: "Conectando" },
14
+ 3: { code: "disconnecting", label: "Desconectando" },
15
+ });
16
+
17
+ function databaseState(readyState = mongoose.connection.readyState) {
18
+ return DATABASE_STATES[readyState] || {
19
+ code: "unknown",
20
+ label: "Status desconhecido",
21
+ };
22
+ }
23
+
24
+ function readinessSummary(readyState = mongoose.connection.readyState) {
25
+ const database = databaseState(readyState);
26
+ const ready = readyState === 1;
27
+ return {
28
+ status: ready ? "ready" : "not_ready",
29
+ service: ctx.config.serviceName,
30
+ database: {
31
+ ready,
32
+ state: database.code,
33
+ },
34
+ };
35
+ }
36
+
37
+ function versionSummary() {
38
+ return {
39
+ status: "ok",
40
+ service: ctx.config.serviceName,
41
+ ...runtimeSummary(),
13
42
  };
14
- const dbState = mongoose.connection.readyState;
43
+ }
44
+
45
+ router.get("/", (_req, res) => {
46
+ const database = databaseState();
15
47
  res.status(200).json({
16
48
  message: `${ctx.config.serviceName} rodando - vs ${ctx.config.serviceVersion}`,
17
- database: states[dbState] || "Status desconhecido",
49
+ database: database.label,
18
50
  });
19
51
  });
20
52
 
53
+ router.get("/health/ready", (_req, res) => {
54
+ const summary = readinessSummary();
55
+ res.status(summary.database.ready ? 200 : 503).json(summary);
56
+ });
57
+
58
+ router.get("/health/version", (_req, res) => {
59
+ res.status(200).json(versionSummary());
60
+ });
61
+
21
62
  module.exports = router;
63
+ module.exports.databaseState = databaseState;
64
+ module.exports.readinessSummary = readinessSummary;
65
+ module.exports.versionSummary = versionSummary;
@@ -4,6 +4,12 @@ const { mongo } = require("mongoose");
4
4
 
5
5
  const DIGITALOCEAN_API = "https://api.digitalocean.com/v2";
6
6
  const DIGITALOCEAN_HOST_SUFFIX = ".mongo.ondigitalocean.com";
7
+ const DIGITALOCEAN_RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
8
+ const DIGITALOCEAN_SERVER_STATUS = new Set([500, 502, 503, 504]);
9
+
10
+ function delay(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
7
13
 
8
14
  function randomSecret(bytes = 32) {
9
15
  return require("node:crypto").randomBytes(bytes).toString("base64url");
@@ -177,58 +183,108 @@ async function digitalOceanRequest(
177
183
  body,
178
184
  expected = [200],
179
185
  },
180
- { fetchImpl = globalThis.fetch } = {},
186
+ {
187
+ fetchImpl = globalThis.fetch,
188
+ sleepImpl = delay,
189
+ retryAttempts = 3,
190
+ retryBaseDelayMs = 1000,
191
+ } = {},
181
192
  ) {
182
193
  if (!fetchImpl) throw new Error("fetch não disponível para a DigitalOcean API.");
183
- const controller = new AbortController();
184
- const timeout = setTimeout(() => controller.abort(), 20000);
185
- let response;
186
- try {
187
- response = await fetchImpl(
188
- `${DIGITALOCEAN_API}/databases/${encodeURIComponent(clusterId)}${path}`,
189
- {
190
- method,
191
- headers: {
192
- Authorization: `Bearer ${token}`,
193
- Accept: "application/json",
194
- ...(body ? { "Content-Type": "application/json" } : {}),
194
+ const attempts = Math.max(1, Number(retryAttempts) || 3);
195
+
196
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
197
+ const controller = new AbortController();
198
+ const timeout = setTimeout(() => controller.abort(), 20000);
199
+ let response;
200
+ try {
201
+ response = await fetchImpl(
202
+ `${DIGITALOCEAN_API}/databases/${encodeURIComponent(clusterId)}${path}`,
203
+ {
204
+ method,
205
+ headers: {
206
+ Authorization: `Bearer ${token}`,
207
+ Accept: "application/json",
208
+ ...(body ? { "Content-Type": "application/json" } : {}),
209
+ },
210
+ body: body ? JSON.stringify(body) : undefined,
211
+ signal: controller.signal,
195
212
  },
196
- body: body ? JSON.stringify(body) : undefined,
197
- signal: controller.signal,
198
- },
199
- );
200
- } catch (error) {
201
- if (error.name === "AbortError") {
202
- throw new Error("Timeout ao acessar a DigitalOcean API.");
213
+ );
214
+ } catch (error) {
215
+ if (attempt < attempts) {
216
+ await sleepImpl(Math.min(10000, retryBaseDelayMs * 2 ** (attempt - 1)));
217
+ continue;
218
+ }
219
+ if (error.name === "AbortError") {
220
+ throw new Error("Timeout ao acessar a DigitalOcean API.");
221
+ }
222
+ throw new Error(`Falha ao acessar a DigitalOcean API: ${error.message}`);
223
+ } finally {
224
+ clearTimeout(timeout);
225
+ }
226
+
227
+ const payload = await response.json().catch(() => ({}));
228
+ if (expected.includes(response.status)) {
229
+ return { status: response.status, payload };
203
230
  }
204
- throw new Error(`Falha ao acessar a DigitalOcean API: ${error.message}`);
205
- } finally {
206
- clearTimeout(timeout);
207
- }
208
231
 
209
- const payload = await response.json().catch(() => ({}));
210
- if (!expected.includes(response.status)) {
232
+ if (
233
+ DIGITALOCEAN_RETRYABLE_STATUS.has(response.status)
234
+ && attempt < attempts
235
+ ) {
236
+ const retryAfter = Number(response.headers?.get?.("retry-after"));
237
+ const backoff = Number.isFinite(retryAfter) && retryAfter > 0
238
+ ? retryAfter * 1000
239
+ : retryBaseDelayMs * 2 ** (attempt - 1);
240
+ await sleepImpl(Math.min(10000, backoff));
241
+ continue;
242
+ }
243
+
244
+ const providerMessage = String(payload.message || payload.id || "erro")
245
+ .replace(/[\r\n]+/g, " ")
246
+ .slice(0, 300);
247
+ const providerRequestId = String(payload.request_id || "")
248
+ .replace(/[^A-Za-z0-9_.:-]/g, "")
249
+ .slice(0, 120);
211
250
  const error = new Error(
212
- `DigitalOcean API rejeitou ${method} ${path}: HTTP ${response.status} (${payload.id || "erro"}).`,
251
+ `DigitalOcean API rejeitou ${method} ${path}: HTTP ${response.status} (${providerMessage})`
252
+ + (providerRequestId ? ` [requestId: ${providerRequestId}]` : "")
253
+ + ".",
213
254
  );
214
255
  error.status = response.status;
215
256
  error.payload = payload;
257
+ error.providerRequestId = providerRequestId || undefined;
216
258
  throw error;
217
259
  }
218
- return { status: response.status, payload };
260
+
261
+ throw new Error("DigitalOcean API não concluiu a solicitação.");
219
262
  }
220
263
 
221
264
  async function ensureDigitalOceanDatabase(options, deps) {
222
265
  const path = `/dbs/${encodeURIComponent(options.dbName)}`;
266
+ let exists = false;
223
267
  try {
224
268
  await digitalOceanRequest(
225
269
  { ...options, path, expected: [200] },
226
270
  deps,
227
271
  );
228
- return;
272
+ exists = true;
229
273
  } catch (error) {
230
- if (error.status !== 404) throw error;
274
+ if (error.status === 404) {
275
+ exists = false;
276
+ } else if (DIGITALOCEAN_SERVER_STATUS.has(error.status)) {
277
+ const listed = await digitalOceanRequest(
278
+ { ...options, path: "/dbs", expected: [200] },
279
+ deps,
280
+ );
281
+ exists = Array.isArray(listed.payload.dbs)
282
+ && listed.payload.dbs.some((item) => item?.name === options.dbName);
283
+ } else {
284
+ throw error;
285
+ }
231
286
  }
287
+ if (exists) return;
232
288
 
233
289
  await digitalOceanRequest(
234
290
  {
@@ -253,15 +309,28 @@ function digitalOceanUserSettings(dbName) {
253
309
 
254
310
  async function provisionDigitalOceanUser(options, deps) {
255
311
  const userPath = `/users/${encodeURIComponent(options.username)}`;
256
- let exists = true;
312
+ let exists = false;
257
313
  try {
258
314
  await digitalOceanRequest(
259
315
  { ...options, path: userPath, expected: [200] },
260
316
  deps,
261
317
  );
318
+ exists = true;
262
319
  } catch (error) {
263
- if (error.status !== 404) throw error;
264
- exists = false;
320
+ if (error.status === 404) {
321
+ exists = false;
322
+ } else if (DIGITALOCEAN_SERVER_STATUS.has(error.status)) {
323
+ const listed = await digitalOceanRequest(
324
+ { ...options, path: "/users", expected: [200] },
325
+ deps,
326
+ );
327
+ exists = Array.isArray(listed.payload.users)
328
+ && listed.payload.users.some(
329
+ (item) => item?.name === options.username,
330
+ );
331
+ } else {
332
+ throw error;
333
+ }
265
334
  }
266
335
 
267
336
  const settings = digitalOceanUserSettings(options.dbName);
@@ -378,6 +447,8 @@ async function provisionMongo(options, deps = {}) {
378
447
  module.exports = {
379
448
  DIGITALOCEAN_API,
380
449
  DIGITALOCEAN_HOST_SUFFIX,
450
+ DIGITALOCEAN_RETRYABLE_STATUS,
451
+ DIGITALOCEAN_SERVER_STATUS,
381
452
  mongoIdentity,
382
453
  mongoHost,
383
454
  resolveMongoProvisioner,