@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/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
|
|
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 =
|
|
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
package/src/api/auth.routes.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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)
|
|
50
|
+
return mongoHost(adminUri).endsWith(DIGITALOCEAN_HOST_SUFFIX)
|
|
51
|
+
? "digitalocean"
|
|
52
|
+
: "native";
|
|
78
53
|
}
|
|
79
54
|
|
|
80
|
-
function buildRuntimeMongoUri({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
163
|
-
const
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
315
|
+
{ ...options, path: userPath, expected: [200] },
|
|
335
316
|
deps,
|
|
336
317
|
);
|
|
318
|
+
exists = true;
|
|
337
319
|
} catch (error) {
|
|
338
|
-
if (error.status === 404)
|
|
339
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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?.
|
|
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
|
|
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
|
|
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,
|