@oondemand/oon-core-back 0.6.9 → 0.6.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ const { assertMongoIdentity, recoveryRequired } = require("./mongoProvisioning");
4
+
5
+ // The store must create the intent exclusively, then replace it by resourceVersion.
6
+ // A present record without a credential is an interrupted operation, never absence.
7
+ async function resolveMongoCredential({ identity, read, begin, persist, provision, validate }) {
8
+ const record = await read();
9
+ if (record !== null) {
10
+ if (!record.mongoUri) throw recoveryRequired("MONGO_CREDENTIAL_MISSING");
11
+ assertMongoIdentity(record.mongoUri, identity);
12
+ try { await validate(record.mongoUri); }
13
+ catch { throw recoveryRequired("MONGO_PERSISTED_CREDENTIAL_INVALID"); }
14
+ return { mongoUri: record.mongoUri, created: false };
15
+ }
16
+
17
+ await begin();
18
+ let captured;
19
+ try {
20
+ const result = await provision(async (uri) => {
21
+ assertMongoIdentity(uri, identity);
22
+ await persist(uri);
23
+ captured = uri;
24
+ });
25
+ if (!captured || result.mongoUri !== captured) throw recoveryRequired();
26
+ return { mongoUri: captured, created: true };
27
+ } catch {
28
+ // Do not serialize driver/provider errors, URIs, tokens or creation responses.
29
+ throw recoveryRequired("MONGO_PROVISIONING_INCOMPLETE");
30
+ }
31
+ }
32
+
33
+ module.exports = { resolveMongoCredential };
@@ -1,12 +1,11 @@
1
1
  "use strict";
2
2
 
3
- const { mongo } = require("mongoose");
3
+ const defaultMongoClient = () => require("mongoose").mongo.MongoClient;
4
4
  const { technicalIdentity } = require("../core/technicalIdentity");
5
5
 
6
6
  const DIGITALOCEAN_API = "https://api.digitalocean.com/v2";
7
7
  const DIGITALOCEAN_HOST_SUFFIX = ".mongo.ondigitalocean.com";
8
8
  const DIGITALOCEAN_RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
9
- const DIGITALOCEAN_SERVER_STATUS = new Set([500, 502, 503, 504]);
10
9
 
11
10
  function delay(ms) {
12
11
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -43,46 +42,58 @@ function mongoIdentity(appCode, environment, { instanceUid } = {}) {
43
42
  };
44
43
  }
45
44
 
46
- function mongoHost(uri) {
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");
47
60
  try {
48
- return new URL(uri).hostname.toLowerCase();
49
- } catch {
50
- throw new Error("MONGO_ADMIN_URI inválida.");
51
- }
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
+ function mongoHost(uri) {
67
+ const parsed = parseMongoUri(uri);
68
+ return parsed.hosts.split(",")[0].replace(/:\d+$/, "").toLowerCase();
52
69
  }
53
70
 
54
71
  function resolveMongoProvisioner({ adminUri, provisioner = "auto" }) {
55
72
  const normalized = String(provisioner || "auto").trim().toLowerCase();
56
73
  if (!["auto", "native", "digitalocean"].includes(normalized)) {
57
- throw new Error(
58
- `MONGO_PROVISIONER inválido: ${normalized}. Use auto, native ou digitalocean.`,
59
- );
74
+ throw recoveryRequired("MONGO_PROVISIONER_INVALID");
60
75
  }
61
76
  if (normalized !== "auto") return normalized;
62
- return mongoHost(adminUri).endsWith(DIGITALOCEAN_HOST_SUFFIX)
63
- ? "digitalocean"
64
- : "native";
77
+ return mongoHost(adminUri).endsWith(DIGITALOCEAN_HOST_SUFFIX) ? "digitalocean" : "native";
65
78
  }
66
79
 
67
- function buildRuntimeMongoUri({
68
- adminUri,
69
- username,
70
- password,
71
- dbName,
72
- authSource,
73
- }) {
74
- const uri = new URL(adminUri);
75
- uri.username = username;
76
- uri.password = password;
77
- uri.pathname = `/${dbName}`;
78
- uri.searchParams.set("authSource", authSource);
79
- return uri.toString();
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
+ }
80
91
  }
81
92
 
82
93
  async function validateMongoUri(
83
94
  mongoUri,
84
95
  {
85
- MongoClient = mongo.MongoClient,
96
+ MongoClient = defaultMongoClient(),
86
97
  timeoutMs = 15000,
87
98
  writeProbe = true,
88
99
  } = {},
@@ -93,9 +104,16 @@ async function validateMongoUri(
93
104
  maxPoolSize: 2,
94
105
  });
95
106
 
96
- await client.connect();
97
107
  try {
108
+ await client.connect();
98
109
  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
+ }
99
117
  await db.command({ ping: 1 });
100
118
  if (writeProbe) {
101
119
  const probe = db.collection("__oon_runtime_probe");
@@ -133,7 +151,7 @@ async function waitForMongoUri(
133
151
  }
134
152
  }
135
153
  }
136
- const reason = lastError?.codeName || lastError?.code || lastError?.name;
154
+ const reason = [13, 18].includes(lastError?.code) ? lastError.code : undefined;
137
155
  throw new Error(
138
156
  `A credencial derivada do MongoDB não passou no teste de leitura e escrita${
139
157
  reason ? ` (${reason})` : ""
@@ -141,16 +159,23 @@ async function waitForMongoUri(
141
159
  );
142
160
  }
143
161
 
144
- async function provisionNativeMongo({ adminUri, appCode, environment, instanceUid }) {
162
+ async function provisionNativeMongo({ adminUri, appCode, environment, instanceUid }, deps = {}) {
145
163
  const identity = mongoIdentity(appCode, environment, { instanceUid });
146
164
  const { dbName, username } = identity;
147
165
  const password = randomSecret(30);
148
- const client = new mongo.MongoClient(adminUri, {
149
- serverSelectionTimeoutMS: 15000,
166
+ const MongoClient = deps.MongoClient || defaultMongoClient();
167
+ const client = new MongoClient(adminUri, {
168
+ serverSelectionTimeoutMS: 15000, connectTimeoutMS: 15000, maxPoolSize: 2,
150
169
  });
151
- await client.connect();
152
170
  try {
171
+ await client.connect();
153
172
  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);
154
179
  try {
155
180
  await db.command({
156
181
  createUser: username,
@@ -163,11 +188,7 @@ async function provisionNativeMongo({ adminUri, appCode, environment, instanceUi
163
188
  error.code === 51003 ||
164
189
  /already exists/i.test(error.message);
165
190
  if (!alreadyExists) throw error;
166
- await db.command({
167
- updateUser: username,
168
- pwd: password,
169
- roles: [{ role: "readWrite", db: dbName }],
170
- });
191
+ throw recoveryRequired();
171
192
  }
172
193
  } finally {
173
194
  await client.close();
@@ -207,7 +228,7 @@ async function digitalOceanRequest(
207
228
  } = {},
208
229
  ) {
209
230
  if (!fetchImpl) throw new Error("fetch não disponível para a DigitalOcean API.");
210
- const attempts = Math.max(1, Number(retryAttempts) || 3);
231
+ const attempts = method === "GET" ? Math.max(1, Number(retryAttempts) || 3) : 1;
211
232
 
212
233
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
213
234
  const controller = new AbortController();
@@ -225,6 +246,7 @@ async function digitalOceanRequest(
225
246
  },
226
247
  body: body ? JSON.stringify(body) : undefined,
227
248
  signal: controller.signal,
249
+ redirect: "error",
228
250
  },
229
251
  );
230
252
  } catch (error) {
@@ -235,7 +257,7 @@ async function digitalOceanRequest(
235
257
  if (error.name === "AbortError") {
236
258
  throw new Error("Timeout ao acessar a DigitalOcean API.");
237
259
  }
238
- throw new Error(`Falha ao acessar a DigitalOcean API: ${error.message}`);
260
+ throw recoveryRequired("MONGO_PROVIDER_REQUEST_FAILED");
239
261
  } finally {
240
262
  clearTimeout(timeout);
241
263
  }
@@ -257,20 +279,8 @@ async function digitalOceanRequest(
257
279
  continue;
258
280
  }
259
281
 
260
- const providerMessage = String(payload.message || payload.id || "erro")
261
- .replace(/[\r\n]+/g, " ")
262
- .slice(0, 300);
263
- const providerRequestId = String(payload.request_id || "")
264
- .replace(/[^A-Za-z0-9_.:-]/g, "")
265
- .slice(0, 120);
266
- const error = new Error(
267
- `DigitalOcean API rejeitou ${method} ${path}: HTTP ${response.status} (${providerMessage})`
268
- + (providerRequestId ? ` [requestId: ${providerRequestId}]` : "")
269
- + ".",
270
- );
282
+ const error = recoveryRequired(`MONGO_PROVIDER_HTTP_${response.status}`);
271
283
  error.status = response.status;
272
- error.payload = payload;
273
- error.providerRequestId = providerRequestId || undefined;
274
284
  throw error;
275
285
  }
276
286
 
@@ -289,19 +299,14 @@ async function ensureDigitalOceanDatabase(options, deps) {
289
299
  } catch (error) {
290
300
  if (error.status === 404) {
291
301
  exists = false;
292
- } else if (DIGITALOCEAN_SERVER_STATUS.has(error.status)) {
293
- const listed = await digitalOceanRequest(
294
- { ...options, path: "/dbs", expected: [200] },
295
- deps,
296
- );
297
- exists = Array.isArray(listed.payload.dbs)
298
- && listed.payload.dbs.some((item) => item?.name === options.dbName);
299
302
  } else {
300
303
  throw error;
301
304
  }
302
305
  }
303
306
  if (exists) return;
304
307
 
308
+ // Refuse a pre-existing/ambiguous user before creating any database resource.
309
+ await assertDigitalOceanUserAbsent(options, deps);
305
310
  await digitalOceanRequest(
306
311
  {
307
312
  ...options,
@@ -323,70 +328,35 @@ function digitalOceanUserSettings(dbName) {
323
328
  };
324
329
  }
325
330
 
326
- async function provisionDigitalOceanUser(options, deps) {
327
- const userPath = `/users/${encodeURIComponent(options.username)}`;
328
- let exists = false;
331
+ async function assertDigitalOceanUserAbsent(options, deps) {
329
332
  try {
330
333
  await digitalOceanRequest(
331
- { ...options, path: userPath, expected: [200] },
334
+ { ...options, path: `/users/${encodeURIComponent(options.username)}`, expected: [200] },
332
335
  deps,
333
336
  );
334
- exists = true;
335
337
  } catch (error) {
336
- if (error.status === 404) {
337
- exists = false;
338
- } else if (DIGITALOCEAN_SERVER_STATUS.has(error.status)) {
339
- const listed = await digitalOceanRequest(
340
- { ...options, path: "/users", expected: [200] },
341
- deps,
342
- );
343
- exists = Array.isArray(listed.payload.users)
344
- && listed.payload.users.some(
345
- (item) => item?.name === options.username,
346
- );
347
- } else {
348
- throw error;
349
- }
338
+ if (error.status === 404) return;
339
+ throw error;
350
340
  }
341
+ throw recoveryRequired();
342
+ }
351
343
 
352
- const settings = digitalOceanUserSettings(options.dbName);
353
- let result;
354
- if (!exists) {
355
- result = await digitalOceanRequest(
356
- {
357
- ...options,
358
- path: "/users",
359
- method: "POST",
360
- body: { name: options.username, settings },
361
- expected: [201, 202],
362
- },
363
- deps,
364
- );
365
- } else {
366
- await digitalOceanRequest(
367
- {
368
- ...options,
369
- path: userPath,
370
- method: "PUT",
371
- body: { settings },
372
- expected: [200, 202],
373
- },
374
- deps,
375
- );
376
- result = await digitalOceanRequest(
377
- {
378
- ...options,
379
- path: `${userPath}/reset_auth`,
380
- method: "POST",
381
- body: {},
382
- expected: [200, 201, 202],
383
- },
384
- deps,
385
- );
386
- }
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
+ );
387
357
 
388
358
  const user = result.payload.user;
389
- if (!user?.password) {
359
+ if (user?.name !== options.username || typeof user?.password !== "string" || !user.password) {
390
360
  throw new Error(
391
361
  "DigitalOcean API não retornou a senha do usuário. O token precisa do escopo database:view_credentials.",
392
362
  );
@@ -424,16 +394,20 @@ async function provisionDigitalOceanMongo(
424
394
  dbName,
425
395
  username,
426
396
  };
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" });
427
400
  await ensureDigitalOceanDatabase(apiOptions, deps);
428
401
  const user = await provisionDigitalOceanUser(apiOptions, deps);
429
402
  const mongoUri = buildRuntimeMongoUri({
430
403
  adminUri,
431
- username: user.name || username,
404
+ username,
432
405
  password: user.password,
433
406
  dbName,
434
407
  authSource: "admin",
435
408
  });
436
409
 
410
+ await deps.persistCredential(mongoUri);
437
411
  await waitForMongoUri(mongoUri, {
438
412
  validate: deps.validateMongoUri || validateMongoUri,
439
413
  attempts: deps.validationAttempts,
@@ -442,7 +416,7 @@ async function provisionDigitalOceanMongo(
442
416
 
443
417
  return {
444
418
  dbName,
445
- username: user.name || username,
419
+ username,
446
420
  ...(identity.mode === "canonical"
447
421
  ? { instanceUid: identity.instanceUid, technicalIdentityVersion: identity.identityVersion }
448
422
  : {}),
@@ -456,7 +430,7 @@ async function provisionMongo(options, deps = {}) {
456
430
  if (provider === "digitalocean") {
457
431
  return provisionDigitalOceanMongo(options, deps);
458
432
  }
459
- const result = await provisionNativeMongo(options);
433
+ const result = await provisionNativeMongo(options, deps);
460
434
  await waitForMongoUri(result.mongoUri, {
461
435
  validate: deps.validateMongoUri || validateMongoUri,
462
436
  attempts: deps.validationAttempts,
@@ -466,10 +440,10 @@ async function provisionMongo(options, deps = {}) {
466
440
  }
467
441
 
468
442
  module.exports = {
443
+ recoveryRequired, parseMongoUri, assertMongoIdentity,
469
444
  DIGITALOCEAN_API,
470
445
  DIGITALOCEAN_HOST_SUFFIX,
471
446
  DIGITALOCEAN_RETRYABLE_STATUS,
472
- DIGITALOCEAN_SERVER_STATUS,
473
447
  mongoIdentity,
474
448
  mongoHost,
475
449
  resolveMongoProvisioner,
@@ -483,4 +457,4 @@ module.exports = {
483
457
  provisionDigitalOceanUser,
484
458
  provisionDigitalOceanMongo,
485
459
  provisionMongo,
486
- };
460
+ };
@@ -4,7 +4,8 @@ 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 { provisionMongo, validateMongoUri } = require("./mongoProvisioning");
7
+ const { resolveMongoCredential } = require("./mongoCredentialLifecycle");
8
+ const { provisionMongo, validateMongoUri, mongoIdentity, recoveryRequired } = require("./mongoProvisioning");
8
9
  const { canonicalizeDeliveryContext } = require("./technicalIdentity");
9
10
 
10
11
  const RESOURCE_PROFILES = {
@@ -32,9 +33,9 @@ function applyYaml(yaml) {
32
33
  }
33
34
  function readSecret(namespace, name, key) {
34
35
  try {
35
- const value = kubectl(["-n", namespace, "get", "secret", name, "-o", `jsonpath={.data.${key}}`], { capture: true });
36
+ const value = kubectl(["-n", namespace, "get", "secret", name, "--ignore-not-found", "-o", `jsonpath={.data.${key}}`], { capture: true });
36
37
  return value ? Buffer.from(value, "base64").toString("utf8") : null;
37
- } catch { return null; }
38
+ } catch { throw recoveryRequired("MONGO_SECRET_READ_FAILED"); }
38
39
  }
39
40
 
40
41
  function canonicalRuntimeContext(context) {
@@ -177,10 +178,11 @@ function copyPullSecret(namespace, infra) {
177
178
  } catch (error) { if (!/not found/i.test(error.message)) throw error; }
178
179
  }
179
180
 
180
- function applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets = {} }) {
181
+ function applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets = {}, record, writeRecord }) {
181
182
  const normalized = canonicalRuntimeContext(context);
182
183
  const identity = normalized.deliveryTechnicalIdentity;
183
184
  const literals = {
185
+ ...extraSecrets,
184
186
  MONGO_URI: mongoUri,
185
187
  NODE_ENV: "production",
186
188
  APP_ENVIRONMENT: normalized.environment,
@@ -195,12 +197,15 @@ function applyRuntimeSecret({ context, mongoUri, encryptionKey, extraSecrets = {
195
197
  OON_TECHNICAL_IDENTITY_VERSION: String(identity.version),
196
198
  }
197
199
  : {}),
198
- ...extraSecrets,
199
200
  };
200
- const args = ["-n", normalized.namespace, "create", "secret", "generic", identity.secret];
201
- for (const [key, value] of Object.entries(literals)) args.push(`--from-literal=${key}=${value}`);
202
- args.push("--dry-run=client", "-o", "yaml");
203
- applyYaml(kubectl(args, { capture: true }));
201
+ writeRecord("replace", {
202
+ ...record,
203
+ metadata: { ...record.metadata, annotations: {
204
+ ...record.metadata.annotations, "oon.runtime/mongo-state": "ready",
205
+ } },
206
+ data: { ...record.data, ...Object.fromEntries(Object.entries(literals)
207
+ .map(([key, value]) => [key, Buffer.from(String(value)).toString("base64")])) },
208
+ });
204
209
  }
205
210
 
206
211
  async function provisionRuntime({ context, extraSecrets = {} }) {
@@ -234,45 +239,56 @@ async function provisionRuntime({ context, extraSecrets = {} }) {
234
239
  "DIGITALOCEAN_DATABASE_CLUSTER_ID",
235
240
  );
236
241
 
237
- let mongoUri = readSecret(normalized.namespace, identity.secret, "MONGO_URI");
238
- let databaseName = "";
239
- let credentialRotated = false;
240
-
241
- if (mongoUri) {
242
- try {
243
- await validateMongoUri(mongoUri);
244
- } catch (error) {
245
- console.warn(
246
- "A credencial MongoDB persistida não passou na validação; " +
247
- "ela será reprovisionada.",
248
- );
249
- mongoUri = null;
250
- credentialRotated = true;
251
- }
252
- }
253
-
254
- if (!mongoUri) {
255
- const provisioned = await provisionMongo({
256
- adminUri,
257
- appCode: normalized.appCode,
258
- environment: normalized.environment,
259
- ...(identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}),
260
- provisioner: mongoProvisioner,
261
- digitalOceanToken,
262
- digitalOceanClusterId,
242
+ // Read errors are distinct from confirmed absence. Never provision on a failed GET.
243
+ let record;
244
+ try {
245
+ const raw = kubectl(["-n", normalized.namespace, "get", "secret", identity.secret,
246
+ "--ignore-not-found", "-o", "json"], { capture: true });
247
+ record = raw ? JSON.parse(raw) : null;
248
+ } catch { throw recoveryRequired("MONGO_SECRET_READ_FAILED"); }
249
+ const decode = (key) => record?.data?.[key]
250
+ ? Buffer.from(record.data[key], "base64").toString("utf8") : null;
251
+ const encryptionKey = record === null ? randomSecret(48) : decode("INSTANCE_CREDENTIAL_ENCRYPTION_KEY");
252
+ if (!encryptionKey) throw recoveryRequired("MONGO_ENCRYPTION_KEY_MISSING");
253
+ const writeRecord = (operation, value) => {
254
+ // Secrets travel through stdin and captured output, never inherited stderr/argv.
255
+ const result = spawnSync("kubectl", [operation, "-f", "-", "-o", "json"], {
256
+ input: JSON.stringify(value), encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
263
257
  });
264
- mongoUri = provisioned.mongoUri;
265
- databaseName = provisioned.dbName;
266
- credentialRotated = true;
267
- }
268
-
269
- const encryptionKey =
270
- readSecret(
271
- normalized.namespace,
272
- identity.secret,
273
- "INSTANCE_CREDENTIAL_ENCRYPTION_KEY",
274
- ) || randomSecret(48);
275
- applyRuntimeSecret({ context: normalized, mongoUri, encryptionKey, extraSecrets });
258
+ if (result.status !== 0) throw recoveryRequired("MONGO_SECRET_WRITE_FAILED");
259
+ try { record = JSON.parse(result.stdout); }
260
+ catch { throw recoveryRequired("MONGO_SECRET_RESPONSE_INVALID"); }
261
+ if (!record?.metadata?.resourceVersion) throw recoveryRequired("MONGO_SECRET_RESPONSE_INVALID");
262
+ };
263
+ const mongo = await resolveMongoCredential({
264
+ identity: mongoIdentity(normalized.appCode, normalized.environment,
265
+ identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}),
266
+ read: async () => record === null ? null : { mongoUri: decode("MONGO_URI") },
267
+ begin: async () => writeRecord("create", {
268
+ apiVersion: "v1", kind: "Secret", type: "Opaque",
269
+ metadata: { name: identity.secret, namespace: normalized.namespace,
270
+ annotations: { "oon.runtime/mongo-state": "provisioning" } },
271
+ data: { INSTANCE_CREDENTIAL_ENCRYPTION_KEY: Buffer.from(encryptionKey).toString("base64") },
272
+ }),
273
+ persist: async (uri) => writeRecord("replace", {
274
+ ...record,
275
+ metadata: { ...record.metadata, annotations: {
276
+ ...record.metadata.annotations, "oon.runtime/mongo-state": "credential-captured",
277
+ } },
278
+ data: { ...record.data, MONGO_URI: Buffer.from(uri).toString("base64") },
279
+ }),
280
+ provision: (persistCredential) => provisionMongo({
281
+ adminUri, appCode: normalized.appCode, environment: normalized.environment,
282
+ ...(identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}),
283
+ provisioner: mongoProvisioner, digitalOceanToken, digitalOceanClusterId,
284
+ }, { persistCredential }),
285
+ validate: validateMongoUri,
286
+ });
287
+ const mongoUri = mongo.mongoUri;
288
+ const databaseName = mongoIdentity(normalized.appCode, normalized.environment,
289
+ identity.mode === "canonical" ? { instanceUid: identity.instanceUid } : {}).dbName;
290
+ const credentialRotated = false;
291
+ applyRuntimeSecret({ context: normalized, mongoUri, encryptionKey, extraSecrets, record, writeRecord });
276
292
  return {
277
293
  infra,
278
294
  databaseName,
@@ -349,4 +365,4 @@ module.exports = {
349
365
  deployRuntime,
350
366
  waitHttp,
351
367
  activateMember,
352
- };
368
+ };