relight-cli 0.2.0 → 0.4.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.
@@ -1,13 +1,19 @@
1
1
  import { randomBytes } from "crypto";
2
2
  import { awsQueryApi, awsJsonApi, xmlVal, xmlList, xmlBlock } from "../../clouds/aws.js";
3
- import { getAppConfig, pushAppConfig } from "./app.js";
3
+ import { getCloudMeta, setCloudMeta } from "../../config.js";
4
4
 
5
- function instanceName(appName) {
6
- return `relight-${appName}`;
5
+ var SHARED_INSTANCE = "relight-shared";
6
+
7
+ function userName(name) {
8
+ return `app_${name.replace(/-/g, "_")}`;
9
+ }
10
+
11
+ function dbName(name) {
12
+ return `relight_${name.replace(/-/g, "_")}`;
7
13
  }
8
14
 
9
- function dbName(appName) {
10
- return `relight_${appName.replace(/-/g, "_")}`;
15
+ function isSharedInstance(dbId) {
16
+ return dbId === SHARED_INSTANCE;
11
17
  }
12
18
 
13
19
  async function connectPg(connectionUrl) {
@@ -84,63 +90,6 @@ async function ensureSecurityGroup(cfg) {
84
90
  return newSgId;
85
91
  }
86
92
 
87
- // --- Get DB password from App Runner env ---
88
-
89
- async function getDbPassword(cfg, appName) {
90
- var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
91
-
92
- // Find App Runner service and extract DB_TOKEN
93
- var nextToken = null;
94
- do {
95
- var params = {};
96
- if (nextToken) params.NextToken = nextToken;
97
- var res = await awsJsonApi("AppRunner.ListServices", params, "apprunner", cr, cfg.region);
98
-
99
- var svcName = `relight-${appName}`;
100
- var match = (res.ServiceSummaryList || []).find((s) => s.ServiceName === svcName);
101
- if (match) {
102
- var descRes = await awsJsonApi(
103
- "AppRunner.DescribeService",
104
- { ServiceArn: match.ServiceArn },
105
- "apprunner",
106
- cr,
107
- cfg.region
108
- );
109
- var envVars = descRes.Service?.SourceConfiguration?.ImageRepository?.ImageConfiguration?.RuntimeEnvironmentVariables || {};
110
- if (envVars.DB_TOKEN) return envVars.DB_TOKEN;
111
- throw new Error("DB_TOKEN not found on service.");
112
- }
113
-
114
- nextToken = res.NextToken;
115
- } while (nextToken);
116
-
117
- throw new Error(`Service relight-${appName} not found.`);
118
- }
119
-
120
- // --- Get connection URL ---
121
-
122
- async function getConnectionUrl(cfg, appName, password) {
123
- var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
124
- var instName = instanceName(appName);
125
- var database = dbName(appName);
126
-
127
- var xml = await awsQueryApi(
128
- "DescribeDBInstances",
129
- { DBInstanceIdentifier: instName },
130
- "rds",
131
- cr,
132
- cfg.region
133
- );
134
-
135
- var endpointBlock = xmlBlock(xml, "Endpoint");
136
- if (!endpointBlock) throw new Error("No endpoint found for RDS instance.");
137
-
138
- var host = xmlVal(endpointBlock, "Address");
139
- var port = xmlVal(endpointBlock, "Port") || "5432";
140
-
141
- return `postgresql://relight:${encodeURIComponent(password)}@${host}:${port}/${database}`;
142
- }
143
-
144
93
  // --- Poll for RDS instance to become available ---
145
94
 
146
95
  async function waitForInstance(cfg, instName) {
@@ -166,40 +115,59 @@ async function waitForInstance(cfg, instName) {
166
115
  throw new Error("Timed out waiting for RDS instance to become available.");
167
116
  }
168
117
 
169
- // --- Public API ---
118
+ // --- Get RDS endpoint ---
170
119
 
171
- export async function createDatabase(cfg, appName, opts = {}) {
172
- if (!opts.skipAppConfig) {
173
- var appConfig = await getAppConfig(cfg, appName);
174
- if (!appConfig) {
175
- throw new Error(`App ${appName} not found.`);
176
- }
177
- if (appConfig.dbId) {
178
- throw new Error(`App ${appName} already has a database: ${appConfig.dbId}`);
179
- }
180
- }
120
+ function getRdsEndpoint(xml) {
121
+ var endpointBlock = xmlBlock(xml, "Endpoint");
122
+ if (!endpointBlock) return null;
123
+ var host = xmlVal(endpointBlock, "Address");
124
+ var port = xmlVal(endpointBlock, "Port") || "5432";
125
+ return { host, port };
126
+ }
181
127
 
128
+ // --- Shared instance management ---
129
+
130
+ async function getOrCreateSharedInstance(cfg) {
182
131
  var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
183
- var instName = instanceName(appName);
184
- var database = dbName(appName);
132
+ var meta = getCloudMeta("aws", "sharedDb");
133
+
134
+ if (meta && meta.instance) {
135
+ // Verify instance still exists
136
+ try {
137
+ var xml = await awsQueryApi(
138
+ "DescribeDBInstances",
139
+ { DBInstanceIdentifier: SHARED_INSTANCE },
140
+ "rds",
141
+ cr,
142
+ cfg.region
143
+ );
144
+ var endpoint = getRdsEndpoint(xml);
145
+ if (endpoint && endpoint.host !== meta.host) {
146
+ meta.host = endpoint.host;
147
+ meta.port = endpoint.port;
148
+ setCloudMeta("aws", "sharedDb", meta);
149
+ }
150
+ return meta;
151
+ } catch (e) {
152
+ // Instance gone, recreate
153
+ }
154
+ }
185
155
 
186
- // Ensure security group for public access
156
+ // Create shared RDS instance
187
157
  var sgId = await ensureSecurityGroup(cfg);
158
+ var masterPassword = randomBytes(24).toString("base64url");
188
159
 
189
- // Generate password
190
- var password = randomBytes(24).toString("base64url");
191
-
192
- // Create RDS instance
160
+ process.stderr.write(" Creating shared RDS instance (one-time, takes 5-15 minutes)...\n");
193
161
  await awsQueryApi(
194
162
  "CreateDBInstance",
195
163
  {
196
- DBInstanceIdentifier: instName,
164
+ DBInstanceIdentifier: SHARED_INSTANCE,
197
165
  DBInstanceClass: "db.t4g.micro",
198
166
  Engine: "postgres",
199
167
  EngineVersion: "15",
200
- MasterUsername: "relight",
201
- MasterUserPassword: password,
202
- DBName: database,
168
+ MasterUsername: "relight_admin",
169
+ MasterUserPassword: masterPassword,
170
+ DBName: "postgres",
203
171
  AllocatedStorage: "20",
204
172
  PubliclyAccessible: "true",
205
173
  "VpcSecurityGroupIds.member.1": sgId,
@@ -210,91 +178,139 @@ export async function createDatabase(cfg, appName, opts = {}) {
210
178
  cfg.region
211
179
  );
212
180
 
213
- // Wait for instance to become available (5-15 min)
214
- process.stderr.write(" Waiting for RDS instance (this takes 5-15 minutes)...\n");
215
- await waitForInstance(cfg, instName);
181
+ await waitForInstance(cfg, SHARED_INSTANCE);
216
182
 
217
- // Get connection URL
218
- var connectionUrl = await getConnectionUrl(cfg, appName, password);
183
+ // Get endpoint
184
+ var xml = await awsQueryApi(
185
+ "DescribeDBInstances",
186
+ { DBInstanceIdentifier: SHARED_INSTANCE },
187
+ "rds",
188
+ cr,
189
+ cfg.region
190
+ );
191
+ var endpoint = getRdsEndpoint(xml);
192
+ if (!endpoint || !endpoint.host) throw new Error("No endpoint found for shared RDS instance.");
193
+
194
+ meta = {
195
+ instance: SHARED_INSTANCE,
196
+ host: endpoint.host,
197
+ port: endpoint.port,
198
+ masterPassword,
199
+ };
200
+ setCloudMeta("aws", "sharedDb", meta);
219
201
 
220
- if (!opts.skipAppConfig) {
221
- // Store in app config
222
- appConfig.dbId = instName;
223
- appConfig.dbName = database;
202
+ return meta;
203
+ }
224
204
 
225
- if (!appConfig.envKeys) appConfig.envKeys = [];
226
- if (!appConfig.secretKeys) appConfig.secretKeys = [];
227
- if (!appConfig.env) appConfig.env = {};
205
+ async function connectAsAdmin(cfg) {
206
+ var meta = getCloudMeta("aws", "sharedDb");
207
+ if (!meta || !meta.masterPassword) {
208
+ throw new Error("Shared DB master credentials not found. Run `relight db create` first.");
209
+ }
210
+ var url = `postgresql://relight_admin:${encodeURIComponent(meta.masterPassword)}@${meta.host}:${meta.port}/postgres`;
211
+ var client = await connectPg(url);
212
+ return { client, meta };
213
+ }
228
214
 
229
- appConfig.env["DATABASE_URL"] = connectionUrl;
230
- if (!appConfig.envKeys.includes("DATABASE_URL")) appConfig.envKeys.push("DATABASE_URL");
215
+ async function destroySharedInstanceIfEmpty(cfg) {
216
+ var { client } = await connectAsAdmin(cfg);
217
+ try {
218
+ var res = await client.query(
219
+ "SELECT datname FROM pg_database WHERE datname LIKE 'relight_%'"
220
+ );
221
+ if (res.rows.length > 0) return false;
222
+ } finally {
223
+ await client.end();
224
+ }
231
225
 
232
- appConfig.env["DB_TOKEN"] = "[hidden]";
233
- appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
234
- appConfig.secretKeys.push("DB_TOKEN");
235
- appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
226
+ // No relight databases remain - destroy the shared instance
227
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
228
+ await awsQueryApi(
229
+ "DeleteDBInstance",
230
+ { DBInstanceIdentifier: SHARED_INSTANCE, SkipFinalSnapshot: "true" },
231
+ "rds",
232
+ cr,
233
+ cfg.region
234
+ );
235
+ setCloudMeta("aws", "sharedDb", undefined);
236
+ return true;
237
+ }
238
+
239
+ // --- Public API ---
236
240
 
237
- var newSecrets = { DB_TOKEN: password };
238
- await pushAppConfig(cfg, appName, appConfig, { newSecrets });
241
+ export async function createDatabase(cfg, name, opts = {}) {
242
+ var meta = await getOrCreateSharedInstance(cfg);
243
+ var database = dbName(name);
244
+ var user = userName(name);
245
+ var password = randomBytes(24).toString("base64url");
246
+
247
+ // Connect as admin to create database and user
248
+ var adminUrl = `postgresql://relight_admin:${encodeURIComponent(meta.masterPassword)}@${meta.host}:${meta.port}/postgres`;
249
+ var client = await connectPg(adminUrl);
250
+ try {
251
+ await client.query(`CREATE USER ${user} WITH PASSWORD '${password.replace(/'/g, "''")}'`);
252
+ await client.query(`CREATE DATABASE ${database} OWNER ${user}`);
253
+ } finally {
254
+ await client.end();
239
255
  }
240
256
 
257
+ var connectionUrl = `postgresql://${user}:${encodeURIComponent(password)}@${meta.host}:${meta.port}/${database}`;
258
+
241
259
  return {
242
- dbId: instName,
260
+ dbId: SHARED_INSTANCE,
243
261
  dbName: database,
262
+ dbUser: user,
244
263
  dbToken: password,
245
264
  connectionUrl,
246
265
  };
247
266
  }
248
267
 
249
- export async function destroyDatabase(cfg, appName, opts = {}) {
268
+ export async function destroyDatabase(cfg, name, opts = {}) {
250
269
  var dbId = opts.dbId;
251
270
  if (!dbId) {
252
- var appConfig = await getAppConfig(cfg, appName);
253
- if (!appConfig || !appConfig.dbId) {
254
- throw new Error(`App ${appName} does not have a database.`);
255
- }
256
- dbId = appConfig.dbId;
271
+ throw new Error("dbId is required to destroy an AWS database.");
257
272
  }
258
273
 
259
- var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
260
-
261
- await awsQueryApi(
262
- "DeleteDBInstance",
263
- { DBInstanceIdentifier: dbId, SkipFinalSnapshot: "true" },
264
- "rds",
265
- cr,
266
- cfg.region
267
- );
268
-
269
- if (!opts.dbId) {
270
- delete appConfig.dbId;
271
- delete appConfig.dbName;
274
+ // Legacy per-app instance: delete the whole instance
275
+ if (!isSharedInstance(dbId)) {
276
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
277
+ await awsQueryApi(
278
+ "DeleteDBInstance",
279
+ { DBInstanceIdentifier: dbId, SkipFinalSnapshot: "true" },
280
+ "rds",
281
+ cr,
282
+ cfg.region
283
+ );
284
+ return;
285
+ }
272
286
 
273
- if (appConfig.env) {
274
- delete appConfig.env["DATABASE_URL"];
275
- delete appConfig.env["DB_TOKEN"];
276
- }
277
- if (appConfig.envKeys) appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DATABASE_URL");
278
- if (appConfig.secretKeys) appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
287
+ // Shared instance: drop database and user
288
+ var database = dbName(name);
289
+ var user = userName(name);
279
290
 
280
- await pushAppConfig(cfg, appName, appConfig);
291
+ var { client } = await connectAsAdmin(cfg);
292
+ try {
293
+ // Terminate active connections to the database
294
+ await client.query(
295
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${database}' AND pid <> pg_backend_pid()`
296
+ );
297
+ await client.query(`DROP DATABASE IF EXISTS ${database}`);
298
+ await client.query(`DROP USER IF EXISTS ${user}`);
299
+ } finally {
300
+ await client.end();
281
301
  }
302
+
303
+ // Check if shared instance should be destroyed
304
+ await destroySharedInstanceIfEmpty(cfg);
282
305
  }
283
306
 
284
- export async function getDatabaseInfo(cfg, appName, opts = {}) {
307
+ export async function getDatabaseInfo(cfg, name, opts = {}) {
285
308
  var dbId = opts.dbId;
286
- var dbNameVal;
287
309
  if (!dbId) {
288
- var appConfig = await getAppConfig(cfg, appName);
289
- if (!appConfig || !appConfig.dbId) {
290
- throw new Error(`App ${appName} does not have a database.`);
291
- }
292
- dbId = appConfig.dbId;
293
- dbNameVal = appConfig.dbName;
294
- } else {
295
- dbNameVal = dbName(appName);
310
+ throw new Error("dbId is required to get AWS database info.");
296
311
  }
297
312
 
313
+ var database = dbName(name);
298
314
  var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
299
315
  var xml = await awsQueryApi(
300
316
  "DescribeDBInstances",
@@ -304,17 +320,16 @@ export async function getDatabaseInfo(cfg, appName, opts = {}) {
304
320
  cfg.region
305
321
  );
306
322
 
307
- var endpointBlock = xmlBlock(xml, "Endpoint");
308
- var host = endpointBlock ? xmlVal(endpointBlock, "Address") : null;
309
- var port = endpointBlock ? (xmlVal(endpointBlock, "Port") || "5432") : "5432";
323
+ var endpoint = getRdsEndpoint(xml);
324
+ var displayUser = isSharedInstance(dbId) ? userName(name) : "relight";
310
325
 
311
- var connectionUrl = host
312
- ? `postgresql://relight:****@${host}:${port}/${dbNameVal}`
326
+ var connectionUrl = endpoint
327
+ ? `postgresql://${displayUser}:****@${endpoint.host}:${endpoint.port}/${database}`
313
328
  : null;
314
329
 
315
330
  return {
316
331
  dbId,
317
- dbName: dbNameVal,
332
+ dbName: database,
318
333
  connectionUrl,
319
334
  size: null,
320
335
  numTables: null,
@@ -322,17 +337,12 @@ export async function getDatabaseInfo(cfg, appName, opts = {}) {
322
337
  };
323
338
  }
324
339
 
325
- export async function queryDatabase(cfg, appName, sql, params, opts = {}) {
326
- if (!opts.dbId) {
327
- var appConfig = await getAppConfig(cfg, appName);
328
- if (!appConfig || !appConfig.dbId) {
329
- throw new Error(`App ${appName} does not have a database.`);
330
- }
340
+ export async function queryDatabase(cfg, name, sql, params, opts = {}) {
341
+ if (!opts.connectionUrl) {
342
+ throw new Error("connectionUrl is required to query an AWS database.");
331
343
  }
332
344
 
333
- var password = await getDbPassword(cfg, appName);
334
- var connectionUrl = await getConnectionUrl(cfg, appName, password);
335
- var client = await connectPg(connectionUrl);
345
+ var client = await connectPg(opts.connectionUrl);
336
346
 
337
347
  try {
338
348
  var result = await client.query(sql, params || []);
@@ -345,17 +355,12 @@ export async function queryDatabase(cfg, appName, sql, params, opts = {}) {
345
355
  }
346
356
  }
347
357
 
348
- export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
349
- if (!opts.dbId) {
350
- var appConfig = await getAppConfig(cfg, appName);
351
- if (!appConfig || !appConfig.dbId) {
352
- throw new Error(`App ${appName} does not have a database.`);
353
- }
358
+ export async function importDatabase(cfg, name, sqlContent, opts = {}) {
359
+ if (!opts.connectionUrl) {
360
+ throw new Error("connectionUrl is required to import into an AWS database.");
354
361
  }
355
362
 
356
- var password = await getDbPassword(cfg, appName);
357
- var connectionUrl = await getConnectionUrl(cfg, appName, password);
358
- var client = await connectPg(connectionUrl);
363
+ var client = await connectPg(opts.connectionUrl);
359
364
 
360
365
  try {
361
366
  await client.query(sqlContent);
@@ -364,21 +369,13 @@ export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
364
369
  }
365
370
  }
366
371
 
367
- export async function exportDatabase(cfg, appName, opts = {}) {
368
- var database;
369
- if (!opts.dbId) {
370
- var appConfig = await getAppConfig(cfg, appName);
371
- if (!appConfig || !appConfig.dbId) {
372
- throw new Error(`App ${appName} does not have a database.`);
373
- }
374
- database = appConfig.dbName;
375
- } else {
376
- database = dbName(appName);
372
+ export async function exportDatabase(cfg, name, opts = {}) {
373
+ if (!opts.connectionUrl) {
374
+ throw new Error("connectionUrl is required to export an AWS database.");
377
375
  }
378
376
 
379
- var password = await getDbPassword(cfg, appName);
380
- var connectionUrl = await getConnectionUrl(cfg, appName, password);
381
- var client = await connectPg(connectionUrl);
377
+ var database = dbName(name);
378
+ var client = await connectPg(opts.connectionUrl);
382
379
 
383
380
  try {
384
381
  var tablesRes = await client.query(
@@ -433,68 +430,62 @@ export async function exportDatabase(cfg, appName, opts = {}) {
433
430
  }
434
431
  }
435
432
 
436
- export async function rotateToken(cfg, appName, opts = {}) {
433
+ export async function rotateToken(cfg, name, opts = {}) {
437
434
  var dbId = opts.dbId;
438
435
  if (!dbId) {
439
- var appConfig = await getAppConfig(cfg, appName);
440
- if (!appConfig || !appConfig.dbId) {
441
- throw new Error(`App ${appName} does not have a database.`);
442
- }
443
- dbId = appConfig.dbId;
436
+ throw new Error("dbId is required to rotate an AWS database token.");
444
437
  }
445
438
 
446
- var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
447
439
  var newPassword = randomBytes(24).toString("base64url");
448
-
449
- // Update RDS master password
450
- await awsQueryApi(
451
- "ModifyDBInstance",
452
- {
453
- DBInstanceIdentifier: dbId,
454
- MasterUserPassword: newPassword,
455
- },
456
- "rds",
457
- cr,
458
- cfg.region
459
- );
460
-
461
- // Get connection URL with new password
462
- var connectionUrl = await getConnectionUrl(cfg, appName, newPassword);
463
-
464
- if (!opts.skipAppConfig) {
465
- if (!appConfig) {
466
- appConfig = await getAppConfig(cfg, appName);
440
+ var connectionUrl;
441
+ var database = dbName(name);
442
+
443
+ if (isSharedInstance(dbId)) {
444
+ // Update via admin connection
445
+ var user = userName(name);
446
+ var { client, meta } = await connectAsAdmin(cfg);
447
+ try {
448
+ await client.query(`ALTER USER ${user} WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`);
449
+ } finally {
450
+ await client.end();
467
451
  }
452
+ connectionUrl = `postgresql://${user}:${encodeURIComponent(newPassword)}@${meta.host}:${meta.port}/${database}`;
453
+ } else {
454
+ // Legacy: update RDS master password
455
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
456
+ await awsQueryApi(
457
+ "ModifyDBInstance",
458
+ {
459
+ DBInstanceIdentifier: dbId,
460
+ MasterUserPassword: newPassword,
461
+ },
462
+ "rds",
463
+ cr,
464
+ cfg.region
465
+ );
468
466
 
469
- // Update app config
470
- if (!appConfig.envKeys) appConfig.envKeys = [];
471
- if (!appConfig.secretKeys) appConfig.secretKeys = [];
472
- if (!appConfig.env) appConfig.env = {};
473
-
474
- appConfig.env["DB_TOKEN"] = "[hidden]";
475
- if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
476
- appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
477
-
478
- appConfig.env["DATABASE_URL"] = connectionUrl;
479
- if (!appConfig.envKeys.includes("DATABASE_URL")) appConfig.envKeys.push("DATABASE_URL");
480
-
481
- await pushAppConfig(cfg, appName, appConfig, { newSecrets: { DB_TOKEN: newPassword } });
467
+ var xml = await awsQueryApi(
468
+ "DescribeDBInstances",
469
+ { DBInstanceIdentifier: dbId },
470
+ "rds",
471
+ cr,
472
+ cfg.region
473
+ );
474
+ var endpoint = getRdsEndpoint(xml);
475
+ connectionUrl = endpoint
476
+ ? `postgresql://relight:${encodeURIComponent(newPassword)}@${endpoint.host}:${endpoint.port}/${database}`
477
+ : null;
482
478
  }
483
479
 
484
480
  return { dbToken: newPassword, connectionUrl };
485
481
  }
486
482
 
487
- export async function resetDatabase(cfg, appName, opts = {}) {
488
- if (!opts.dbId) {
489
- var appConfig = await getAppConfig(cfg, appName);
490
- if (!appConfig || !appConfig.dbId) {
491
- throw new Error(`App ${appName} does not have a database.`);
492
- }
483
+ export async function resetDatabase(cfg, name, opts = {}) {
484
+ if (!opts.connectionUrl) {
485
+ throw new Error("connectionUrl is required to reset an AWS database.");
493
486
  }
494
487
 
495
- var password = await getDbPassword(cfg, appName);
496
- var connectionUrl = await getConnectionUrl(cfg, appName, password);
497
- var client = await connectPg(connectionUrl);
488
+ var client = await connectPg(opts.connectionUrl);
498
489
 
499
490
  try {
500
491
  var tablesRes = await client.query(
@@ -149,7 +149,7 @@ export async function removeDomain(cfg, appName, domain) {
149
149
 
150
150
  // --- Pure DNS record operations (for cross-cloud use) ---
151
151
 
152
- export async function addDnsRecord(cfg, domain, target, zone) {
152
+ export async function addDnsRecord(cfg, domain, target, zone, opts = {}) {
153
153
  var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
154
154
  var fqdn = domain.endsWith(".") ? domain : domain + ".";
155
155
  var targetFqdn = target.endsWith(".") ? target : target + ".";