@oondemand/oon-core-back 0.6.12 → 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 CHANGED
@@ -5,7 +5,6 @@ const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const { getContextWithPhase, setState, addEvidence } = require("../src/delivery/api");
7
7
  const { provisionRuntime, deployRuntime, waitHttp, activateMember } = require("../src/delivery/runtime");
8
- const { canonicalizeDeliveryContext } = require("../src/delivery/technicalIdentity");
9
8
 
10
9
  function flag(name, fallback) {
11
10
  const args = process.argv.slice(3);
@@ -17,7 +16,6 @@ function flag(name, fallback) {
17
16
  }
18
17
  function required(value, name) { if (!value) throw new Error(`${name} obrigatório.`); return value; }
19
18
  function secret() { return required(process.env.OON_DELIVERY_SHARED_SECRET, "OON_DELIVERY_SHARED_SECRET"); }
20
- function normalizedContext(value) { return { ...canonicalizeDeliveryContext(value) }; }
21
19
 
22
20
  async function main() {
23
21
  const command = process.argv[2];
@@ -27,8 +25,7 @@ async function main() {
27
25
  if (command === "context") {
28
26
  const phase = flag("--phase", "build");
29
27
  const output = required(flag("--output"), "--output");
30
- const fetched = await getContextWithPhase({ apiUrl: required(apiUrl, "--api"), releaseId: required(releaseId, "--release"), token: secret(), phase });
31
- const context = normalizedContext(fetched);
28
+ const context = await getContextWithPhase({ apiUrl: required(apiUrl, "--api"), releaseId: required(releaseId, "--release"), token: secret(), phase });
32
29
  fs.writeFileSync(path.resolve(output), JSON.stringify(context, null, 2), { mode: 0o600 });
33
30
  return;
34
31
  }
@@ -43,7 +40,7 @@ async function main() {
43
40
  if (command === "deploy") {
44
41
  const contextFile = required(flag("--context"), "--context");
45
42
  const imageDigest = required(flag("--image-digest"), "--image-digest");
46
- const context = normalizedContext(JSON.parse(fs.readFileSync(path.resolve(contextFile), "utf8")));
43
+ const context = JSON.parse(fs.readFileSync(path.resolve(contextFile), "utf8"));
47
44
  let failureState = "falha_infra";
48
45
  try {
49
46
  await setState({ apiUrl, releaseId, token: secret(), etapa: "provisionando", imageDigest });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.6.12",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -4,7 +4,7 @@ const auth = require("../core/middlewares/auth");
4
4
  const ctx = require("../core/context");
5
5
  const GenericError = require("../core/errors/GenericError");
6
6
  const { asyncHandler, sendResponse } = require("../core/utils/helpers");
7
- const { ensureInstance, operationalRequestHeaders } = require("../core/utils/instanceActivation");
7
+ const { ensureInstance, operationalRequestHeaders, authenticationRequestHeaders } = require("../core/utils/instanceActivation");
8
8
  const { findAccessForUser, audit } = require("../core/rbac/storage");
9
9
  const { changeFirstAccessPassword } = require("../core/rbac/identityProvider");
10
10
  const { isLocalRuntime } = require("../core/runtime/localDevelopment");
@@ -76,6 +76,7 @@ function tenantForLogin(req, instance) {
76
76
  // único tenant elegível ou devolve as opções disponíveis.
77
77
  return req.headers[TENANT_HEADER];
78
78
  }
79
+ if (instance.deploymentId) return req.headers[TENANT_HEADER];
79
80
  return instance.tenantId;
80
81
  }
81
82
 
@@ -102,6 +103,7 @@ router.post(
102
103
  const appCode = getAppCode();
103
104
  const tenantId = tenantForLogin(req, instance);
104
105
  try {
106
+ const runtimeHeaders = await authenticationRequestHeaders({ correlationId: req.headers["x-correlation-id"] }, { ensureInstance: async () => instance });
105
107
  const response = await axios.post(
106
108
  `${baseUrl}${LOGIN_PATH}`,
107
109
  {}, // codigoApp: appCode é enviado no header x-app-code.
@@ -109,6 +111,7 @@ router.post(
109
111
  timeout: getTimeout(),
110
112
  maxRedirects: 0,
111
113
  headers: {
114
+ ...runtimeHeaders,
112
115
  Authorization: authorization,
113
116
  "Content-Type": "application/json",
114
117
  "x-app-code": appCode,
@@ -240,3 +243,4 @@ router.post(
240
243
  );
241
244
 
242
245
  module.exports = router;
246
+
@@ -1,3 +1,5 @@
1
+ const { roleByCode, rolePermissions } = require("../rbac/policy");
2
+ const { authenticationRequestHeaders } = require("../utils/instanceActivation");
1
3
  const axios = require("axios");
2
4
  const Helpers = require("../utils/helpers");
3
5
  const ctx = require("../context");
@@ -31,10 +33,12 @@ async function defaultVerifyToken(token, { req } = {}) {
31
33
 
32
34
  let response;
33
35
  try {
36
+ const runtimeHeaders = await authenticationRequestHeaders({ correlationId: req?.headers?.["x-correlation-id"] });
34
37
  response = await axios.get(`${baseUrl}${PERMISSION_PATH}`, {
35
38
  timeout: getTimeout(),
36
39
  maxRedirects: 0,
37
40
  headers: {
41
+ ...runtimeHeaders,
38
42
  Authorization: `Bearer ${token}`,
39
43
  "x-app-code": appCode,
40
44
  ...(requestedTenantId ? { [TENANT_HEADER]: requestedTenantId } : {}),
@@ -119,6 +123,20 @@ async function applyEffectiveAccess(usuario) {
119
123
  rbacMode: "local-development",
120
124
  };
121
125
  }
126
+ if (usuario?.commercialAccess) {
127
+ const authority = usuario.commercialAccess;
128
+ const role = roleByCode(authority.roleCode);
129
+ const cap = authority.functionalPermissions;
130
+ if (!authority.appLicenseId || !authority.grantId || !role || !Array.isArray(cap)
131
+ || cap.some(p => typeof p !== "string" || p === "*" || /^(tenant|publications|homologation|production|apps|users|roles)\./.test(p))) {
132
+ throw Object.assign(Error("Autoridade comercial inválida."), { statusCode: 403, code: "APP_COMMERCIAL_AUTHORITY_INVALID" });
133
+ }
134
+ const local = rolePermissions(role);
135
+ const permissions = [...new Set(cap)].filter(p => local.includes("*") || local.includes(p));
136
+ return { ...usuario, roleCode: role.code, permissions, accessStatus: "active", mustChangePassword: false,
137
+ rbacMode: "app-commercial", commercialAccess: { ...authority,
138
+ roleComplete: !local.includes("*") && local.every(p => permissions.includes(p)) } };
139
+ }
122
140
  const access = await resolveUserAccess(usuario);
123
141
  return {
124
142
  ...usuario,
@@ -193,3 +211,4 @@ module.exports.localDevelopmentVerifyToken = localDevelopmentVerifyToken;
193
211
  module.exports.resolveVerifyToken = resolveVerifyToken;
194
212
  module.exports.applyEffectiveAccess = applyEffectiveAccess;
195
213
  module.exports.isFirstAccessRoute = isFirstAccessRoute;
214
+
@@ -22,15 +22,20 @@ function normalize(value) {
22
22
  }
23
23
 
24
24
  function effectiveRoles(usuario) {
25
- const raw = [usuario?.roleCode, usuario?.tipo, usuario?.perfil]
25
+ const raw = (usuario?.commercialAccess ? [usuario.roleCode] : [usuario?.roleCode, usuario?.tipo, usuario?.perfil])
26
26
  .map(normalize)
27
27
  .filter(Boolean);
28
+ if (usuario?.commercialAccess) return [...new Set(raw)];
28
29
  return [...new Set(raw.flatMap((role) => ROLE_ALIASES[role] || [role]))];
29
30
  }
30
31
 
31
32
  function rbac(roles = []) {
32
33
  return (req, res, next) => {
33
34
  if (!roles.length) return next();
35
+ if (req.usuario?.commercialAccess && !req.usuario.commercialAccess.roleComplete) {
36
+ return Helpers.sendErrorResponse({ res, statusCode: 403,
37
+ code: "APP_COMMERCIAL_PERMISSION_REQUIRED", message: "Esta operação exige permissão explícita." });
38
+ }
34
39
  const allowed = roles.map(normalize);
35
40
  const current = effectiveRoles(req.usuario);
36
41
 
@@ -517,6 +517,24 @@ async function operationalRequestHeaders(options = {}, dependencies = {}) {
517
517
  return headers;
518
518
  }
519
519
 
520
+ // Authentication carries runtime identity, never an implicit functional tenant.
521
+ // The Central decides whether a selected tenant may use this deployment.
522
+ async function authenticationRequestHeaders(options = {}, dependencies = {}) {
523
+ try {
524
+ const instance = await (dependencies.ensureInstance || ensureInstance)();
525
+ if (!operationalIdentity(instance)) return {};
526
+ const headers = await operationalRequestHeaders(options, { ...dependencies, ensureInstance: async () => instance });
527
+ delete headers["x-oon-tenant-id"];
528
+ return headers;
529
+ } catch (error) {
530
+ if (error.statusCode) throw error;
531
+ throw new GenericError("Não foi possível carregar a credencial operacional do runtime.", {
532
+ statusCode: 503,
533
+ code: "OPERATIONAL_CREDENTIAL_UNAVAILABLE",
534
+ });
535
+ }
536
+ }
537
+
520
538
  function validateUrl(value) {
521
539
  const url = new URL(value);
522
540
  if (!["http:", "https:"].includes(url.protocol)) throw new Error();
@@ -616,8 +634,10 @@ module.exports = {
616
634
  instanceHeaders,
617
635
  operationalIdentity,
618
636
  operationalRequestHeaders,
637
+ authenticationRequestHeaders,
619
638
  validateParams,
620
639
  providerPost,
621
640
  providerPayload,
622
641
  event,
623
642
  };
643
+
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
 
3
- const defaultMongoClient = () => require("mongoose").mongo.MongoClient;
4
- const { technicalIdentity } = require("../core/technicalIdentity");
3
+ const { mongo } = require("mongoose");
5
4
 
6
5
  const DIGITALOCEAN_API = "https://api.digitalocean.com/v2";
7
6
  const DIGITALOCEAN_HOST_SUFFIX = ".mongo.ondigitalocean.com";
8
7
  const DIGITALOCEAN_RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
8
+ const DIGITALOCEAN_SERVER_STATUS = new Set([500, 502, 503, 504]);
9
9
 
10
10
  function delay(ms) {
11
11
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -24,76 +24,53 @@ function dnsLabel(value, max = 63) {
24
24
  .replace(/-+$/, "");
25
25
  }
26
26
 
27
- function mongoIdentity(appCode, environment, { instanceUid } = {}) {
28
- if (instanceUid != null) {
29
- const identity = technicalIdentity({ appCode, instanceUid, environment });
30
- return {
31
- mode: "canonical",
32
- identityVersion: identity.version,
33
- instanceUid: identity.instanceUid,
34
- dbName: identity.database,
35
- username: identity.mongoUsername,
36
- };
37
- }
27
+ function mongoIdentity(appCode, environment) {
38
28
  return {
39
- mode: "legacy",
40
29
  dbName: dnsLabel(`${appCode}_${environment}`, 48).replace(/-/g, "_"),
41
30
  username: dnsLabel(`${appCode}-${environment}`, 48),
42
31
  };
43
32
  }
44
33
 
45
- function recoveryRequired(code = "MONGO_RECOVERY_REQUIRED") {
46
- const error = new Error(`${code}: recuperação explícita necessária; credenciais preservadas.`);
47
- error.code = code;
48
- return error;
49
- }
50
-
51
- // WHATWG URL rejects MongoDB seed lists with multiple host:port pairs.
52
- function parseMongoUri(uri) {
53
- const match = typeof uri === "string" && uri.match(/^(mongodb(?:\+srv)?):\/\/([^/?#]+)(?:\/([^?#]*))?(?:\?([^#]*))?$/);
54
- if (!match) throw recoveryRequired("MONGO_URI_INVALID");
55
- const [, scheme, authority, database = "", query = ""] = match;
56
- const at = authority.lastIndexOf("@");
57
- const credentials = at < 0 ? "" : authority.slice(0, at);
58
- const hosts = authority.slice(at + 1);
59
- if (!hosts) throw recoveryRequired("MONGO_URI_INVALID");
60
- try {
61
- return { scheme, hosts, database: decodeURIComponent(database),
62
- username: decodeURIComponent(credentials.split(":")[0]), query: new URLSearchParams(query) };
63
- } catch { throw recoveryRequired("MONGO_URI_INVALID"); }
64
- }
65
-
66
34
  function mongoHost(uri) {
67
- const parsed = parseMongoUri(uri);
68
- return parsed.hosts.split(",")[0].replace(/:\d+$/, "").toLowerCase();
35
+ try {
36
+ return new URL(uri).hostname.toLowerCase();
37
+ } catch {
38
+ throw new Error("MONGO_ADMIN_URI inválida.");
39
+ }
69
40
  }
70
41
 
71
42
  function resolveMongoProvisioner({ adminUri, provisioner = "auto" }) {
72
43
  const normalized = String(provisioner || "auto").trim().toLowerCase();
73
44
  if (!["auto", "native", "digitalocean"].includes(normalized)) {
74
- throw recoveryRequired("MONGO_PROVISIONER_INVALID");
45
+ throw new Error(
46
+ `MONGO_PROVISIONER inválido: ${normalized}. Use auto, native ou digitalocean.`,
47
+ );
75
48
  }
76
49
  if (normalized !== "auto") return normalized;
77
- return mongoHost(adminUri).endsWith(DIGITALOCEAN_HOST_SUFFIX) ? "digitalocean" : "native";
50
+ return mongoHost(adminUri).endsWith(DIGITALOCEAN_HOST_SUFFIX)
51
+ ? "digitalocean"
52
+ : "native";
78
53
  }
79
54
 
80
- function buildRuntimeMongoUri({ adminUri, username, password, dbName, authSource }) {
81
- const parsed = parseMongoUri(adminUri);
82
- parsed.query.set("authSource", authSource);
83
- return `${parsed.scheme}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${parsed.hosts}/${encodeURIComponent(dbName)}?${parsed.query}`;
84
- }
85
-
86
- function assertMongoIdentity(uri, identity) {
87
- const parsed = parseMongoUri(uri);
88
- if (parsed.username !== identity.username || parsed.database !== identity.dbName) {
89
- throw recoveryRequired("MONGO_IDENTITY_MISMATCH");
90
- }
55
+ function buildRuntimeMongoUri({
56
+ adminUri,
57
+ username,
58
+ password,
59
+ dbName,
60
+ authSource,
61
+ }) {
62
+ const uri = new URL(adminUri);
63
+ uri.username = username;
64
+ uri.password = password;
65
+ uri.pathname = `/${dbName}`;
66
+ uri.searchParams.set("authSource", authSource);
67
+ return uri.toString();
91
68
  }
92
69
 
93
70
  async function validateMongoUri(
94
71
  mongoUri,
95
72
  {
96
- MongoClient = defaultMongoClient(),
73
+ MongoClient = mongo.MongoClient,
97
74
  timeoutMs = 15000,
98
75
  writeProbe = true,
99
76
  } = {},
@@ -104,16 +81,9 @@ async function validateMongoUri(
104
81
  maxPoolSize: 2,
105
82
  });
106
83
 
84
+ await client.connect();
107
85
  try {
108
- await client.connect();
109
86
  const db = client.db();
110
- const target = parseMongoUri(mongoUri);
111
- const status = await db.command({ connectionStatus: 1 });
112
- const roles = status?.authInfo?.authenticatedUserRoles;
113
- if (!target.database || !Array.isArray(roles) || !roles.length ||
114
- roles.some(role => role.role !== "readWrite" || role.db !== target.database)) {
115
- throw recoveryRequired("MONGO_RUNTIME_SCOPE_UNSAFE");
116
- }
117
87
  await db.command({ ping: 1 });
118
88
  if (writeProbe) {
119
89
  const probe = db.collection("__oon_runtime_probe");
@@ -151,7 +121,7 @@ async function waitForMongoUri(
151
121
  }
152
122
  }
153
123
  }
154
- const reason = [13, 18].includes(lastError?.code) ? lastError.code : undefined;
124
+ const reason = lastError?.codeName || lastError?.code || lastError?.name;
155
125
  throw new Error(
156
126
  `A credencial derivada do MongoDB não passou no teste de leitura e escrita${
157
127
  reason ? ` (${reason})` : ""
@@ -159,23 +129,15 @@ async function waitForMongoUri(
159
129
  );
160
130
  }
161
131
 
162
- async function provisionNativeMongo({ adminUri, appCode, environment, instanceUid }, deps = {}) {
163
- const identity = mongoIdentity(appCode, environment, { instanceUid });
164
- const { dbName, username } = identity;
132
+ async function provisionNativeMongo({ adminUri, appCode, environment }) {
133
+ const { dbName, username } = mongoIdentity(appCode, environment);
165
134
  const password = randomSecret(30);
166
- const MongoClient = deps.MongoClient || defaultMongoClient();
167
- const client = new MongoClient(adminUri, {
168
- serverSelectionTimeoutMS: 15000, connectTimeoutMS: 15000, maxPoolSize: 2,
135
+ const client = new mongo.MongoClient(adminUri, {
136
+ serverSelectionTimeoutMS: 15000,
169
137
  });
138
+ await client.connect();
170
139
  try {
171
- await client.connect();
172
140
  const db = client.db(dbName);
173
- const existing = await db.command({ usersInfo: username });
174
- if (!Array.isArray(existing.users) || existing.users.length) throw recoveryRequired();
175
- const mongoUri = buildRuntimeMongoUri({ adminUri, username, password, dbName, authSource: dbName });
176
- if (typeof deps.persistCredential !== "function") throw recoveryRequired("MONGO_CREDENTIAL_PERSISTENCE_REQUIRED");
177
- // Capture before createUser: loss of its response never requires password rotation.
178
- await deps.persistCredential(mongoUri);
179
141
  try {
180
142
  await db.command({
181
143
  createUser: username,
@@ -188,7 +150,11 @@ async function provisionNativeMongo({ adminUri, appCode, environment, instanceUi
188
150
  error.code === 51003 ||
189
151
  /already exists/i.test(error.message);
190
152
  if (!alreadyExists) throw error;
191
- throw recoveryRequired();
153
+ await db.command({
154
+ updateUser: username,
155
+ pwd: password,
156
+ roles: [{ role: "readWrite", db: dbName }],
157
+ });
192
158
  }
193
159
  } finally {
194
160
  await client.close();
@@ -197,9 +163,6 @@ async function provisionNativeMongo({ adminUri, appCode, environment, instanceUi
197
163
  return {
198
164
  dbName,
199
165
  username,
200
- ...(identity.mode === "canonical"
201
- ? { instanceUid: identity.instanceUid, technicalIdentityVersion: identity.identityVersion }
202
- : {}),
203
166
  mongoUri: buildRuntimeMongoUri({
204
167
  adminUri,
205
168
  username,
@@ -228,7 +191,7 @@ async function digitalOceanRequest(
228
191
  } = {},
229
192
  ) {
230
193
  if (!fetchImpl) throw new Error("fetch não disponível para a DigitalOcean API.");
231
- const attempts = method === "GET" ? Math.max(1, Number(retryAttempts) || 3) : 1;
194
+ const attempts = Math.max(1, Number(retryAttempts) || 3);
232
195
 
233
196
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
234
197
  const controller = new AbortController();
@@ -246,7 +209,6 @@ async function digitalOceanRequest(
246
209
  },
247
210
  body: body ? JSON.stringify(body) : undefined,
248
211
  signal: controller.signal,
249
- redirect: "error",
250
212
  },
251
213
  );
252
214
  } catch (error) {
@@ -257,7 +219,7 @@ async function digitalOceanRequest(
257
219
  if (error.name === "AbortError") {
258
220
  throw new Error("Timeout ao acessar a DigitalOcean API.");
259
221
  }
260
- throw recoveryRequired("MONGO_PROVIDER_REQUEST_FAILED");
222
+ throw new Error(`Falha ao acessar a DigitalOcean API: ${error.message}`);
261
223
  } finally {
262
224
  clearTimeout(timeout);
263
225
  }
@@ -279,8 +241,20 @@ async function digitalOceanRequest(
279
241
  continue;
280
242
  }
281
243
 
282
- const error = recoveryRequired(`MONGO_PROVIDER_HTTP_${response.status}`);
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);
250
+ const error = new Error(
251
+ `DigitalOcean API rejeitou ${method} ${path}: HTTP ${response.status} (${providerMessage})`
252
+ + (providerRequestId ? ` [requestId: ${providerRequestId}]` : "")
253
+ + ".",
254
+ );
283
255
  error.status = response.status;
256
+ error.payload = payload;
257
+ error.providerRequestId = providerRequestId || undefined;
284
258
  throw error;
285
259
  }
286
260
 
@@ -299,14 +273,19 @@ async function ensureDigitalOceanDatabase(options, deps) {
299
273
  } catch (error) {
300
274
  if (error.status === 404) {
301
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);
302
283
  } else {
303
284
  throw error;
304
285
  }
305
286
  }
306
287
  if (exists) return;
307
288
 
308
- // Refuse a pre-existing/ambiguous user before creating any database resource.
309
- await assertDigitalOceanUserAbsent(options, deps);
310
289
  await digitalOceanRequest(
311
290
  {
312
291
  ...options,
@@ -328,35 +307,70 @@ function digitalOceanUserSettings(dbName) {
328
307
  };
329
308
  }
330
309
 
331
- async function assertDigitalOceanUserAbsent(options, deps) {
310
+ async function provisionDigitalOceanUser(options, deps) {
311
+ const userPath = `/users/${encodeURIComponent(options.username)}`;
312
+ let exists = false;
332
313
  try {
333
314
  await digitalOceanRequest(
334
- { ...options, path: `/users/${encodeURIComponent(options.username)}`, expected: [200] },
315
+ { ...options, path: userPath, expected: [200] },
335
316
  deps,
336
317
  );
318
+ exists = true;
337
319
  } catch (error) {
338
- if (error.status === 404) return;
339
- throw error;
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
+ }
340
334
  }
341
- throw recoveryRequired();
342
- }
343
335
 
344
- async function provisionDigitalOceanUser(options, deps) {
345
- // Recheck after database creation: another publisher may have created the user.
346
- await assertDigitalOceanUserAbsent(options, deps);
347
- const result = await digitalOceanRequest(
348
- {
349
- ...options,
350
- path: "/users",
351
- method: "POST",
352
- body: { name: options.username, settings: digitalOceanUserSettings(options.dbName) },
353
- expected: [201, 202],
354
- },
355
- deps,
356
- );
336
+ const settings = digitalOceanUserSettings(options.dbName);
337
+ let result;
338
+ if (!exists) {
339
+ result = await digitalOceanRequest(
340
+ {
341
+ ...options,
342
+ path: "/users",
343
+ method: "POST",
344
+ body: { name: options.username, settings },
345
+ expected: [201, 202],
346
+ },
347
+ deps,
348
+ );
349
+ } else {
350
+ await digitalOceanRequest(
351
+ {
352
+ ...options,
353
+ path: userPath,
354
+ method: "PUT",
355
+ body: { settings },
356
+ expected: [200, 202],
357
+ },
358
+ deps,
359
+ );
360
+ result = await digitalOceanRequest(
361
+ {
362
+ ...options,
363
+ path: `${userPath}/reset_auth`,
364
+ method: "POST",
365
+ body: {},
366
+ expected: [200, 201, 202],
367
+ },
368
+ deps,
369
+ );
370
+ }
357
371
 
358
372
  const user = result.payload.user;
359
- if (user?.name !== options.username || typeof user?.password !== "string" || !user.password) {
373
+ if (!user?.password) {
360
374
  throw new Error(
361
375
  "DigitalOcean API não retornou a senha do usuário. O token precisa do escopo database:view_credentials.",
362
376
  );
@@ -369,7 +383,6 @@ async function provisionDigitalOceanMongo(
369
383
  adminUri,
370
384
  appCode,
371
385
  environment,
372
- instanceUid,
373
386
  digitalOceanToken,
374
387
  digitalOceanClusterId,
375
388
  },
@@ -386,28 +399,23 @@ async function provisionDigitalOceanMongo(
386
399
  );
387
400
  }
388
401
 
389
- const identity = mongoIdentity(appCode, environment, { instanceUid });
390
- const { dbName, username } = identity;
402
+ const { dbName, username } = mongoIdentity(appCode, environment);
391
403
  const apiOptions = {
392
404
  token: digitalOceanToken,
393
405
  clusterId: digitalOceanClusterId,
394
406
  dbName,
395
407
  username,
396
408
  };
397
- if (typeof deps.persistCredential !== "function") throw recoveryRequired("MONGO_CREDENTIAL_PERSISTENCE_REQUIRED");
398
- // Validate URI construction before any provider mutation.
399
- buildRuntimeMongoUri({ adminUri, username, password: "preflight", dbName, authSource: "admin" });
400
409
  await ensureDigitalOceanDatabase(apiOptions, deps);
401
410
  const user = await provisionDigitalOceanUser(apiOptions, deps);
402
411
  const mongoUri = buildRuntimeMongoUri({
403
412
  adminUri,
404
- username,
413
+ username: user.name || username,
405
414
  password: user.password,
406
415
  dbName,
407
416
  authSource: "admin",
408
417
  });
409
418
 
410
- await deps.persistCredential(mongoUri);
411
419
  await waitForMongoUri(mongoUri, {
412
420
  validate: deps.validateMongoUri || validateMongoUri,
413
421
  attempts: deps.validationAttempts,
@@ -416,10 +424,7 @@ async function provisionDigitalOceanMongo(
416
424
 
417
425
  return {
418
426
  dbName,
419
- username,
420
- ...(identity.mode === "canonical"
421
- ? { instanceUid: identity.instanceUid, technicalIdentityVersion: identity.identityVersion }
422
- : {}),
427
+ username: user.name || username,
423
428
  mongoUri,
424
429
  provider: "digitalocean",
425
430
  };
@@ -430,7 +435,7 @@ async function provisionMongo(options, deps = {}) {
430
435
  if (provider === "digitalocean") {
431
436
  return provisionDigitalOceanMongo(options, deps);
432
437
  }
433
- const result = await provisionNativeMongo(options, deps);
438
+ const result = await provisionNativeMongo(options);
434
439
  await waitForMongoUri(result.mongoUri, {
435
440
  validate: deps.validateMongoUri || validateMongoUri,
436
441
  attempts: deps.validationAttempts,
@@ -440,10 +445,10 @@ async function provisionMongo(options, deps = {}) {
440
445
  }
441
446
 
442
447
  module.exports = {
443
- recoveryRequired, parseMongoUri, assertMongoIdentity,
444
448
  DIGITALOCEAN_API,
445
449
  DIGITALOCEAN_HOST_SUFFIX,
446
450
  DIGITALOCEAN_RETRYABLE_STATUS,
451
+ DIGITALOCEAN_SERVER_STATUS,
447
452
  mongoIdentity,
448
453
  mongoHost,
449
454
  resolveMongoProvisioner,