@geekmidas/cli 0.45.0 → 0.47.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.
Files changed (51) hide show
  1. package/dist/{config-C0b0jdmU.mjs → config-C3LSBNSl.mjs} +2 -2
  2. package/dist/{config-C0b0jdmU.mjs.map → config-C3LSBNSl.mjs.map} +1 -1
  3. package/dist/{config-xVZsRjN7.cjs → config-HYiM3iQJ.cjs} +2 -2
  4. package/dist/{config-xVZsRjN7.cjs.map → config-HYiM3iQJ.cjs.map} +1 -1
  5. package/dist/config.cjs +2 -2
  6. package/dist/config.d.cts +1 -1
  7. package/dist/config.d.mts +1 -1
  8. package/dist/config.mjs +2 -2
  9. package/dist/dokploy-api-4a6h35VY.cjs +3 -0
  10. package/dist/{dokploy-api-BdxOMH_V.cjs → dokploy-api-BnX2OxyF.cjs} +121 -1
  11. package/dist/dokploy-api-BnX2OxyF.cjs.map +1 -0
  12. package/dist/{dokploy-api-DWsqNjwP.mjs → dokploy-api-CMWlWq7-.mjs} +121 -1
  13. package/dist/dokploy-api-CMWlWq7-.mjs.map +1 -0
  14. package/dist/dokploy-api-DQvi9iZa.mjs +3 -0
  15. package/dist/{index-CXa3odEw.d.mts → index-A70abJ1m.d.mts} +598 -46
  16. package/dist/index-A70abJ1m.d.mts.map +1 -0
  17. package/dist/{index-E8Nu2Rxl.d.cts → index-pOA56MWT.d.cts} +598 -46
  18. package/dist/index-pOA56MWT.d.cts.map +1 -0
  19. package/dist/index.cjs +916 -357
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +916 -357
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-D3pA6FfZ.mjs → openapi-C3C-BzIZ.mjs} +2 -2
  24. package/dist/{openapi-D3pA6FfZ.mjs.map → openapi-C3C-BzIZ.mjs.map} +1 -1
  25. package/dist/{openapi-DhcCtKzM.cjs → openapi-D7WwlpPF.cjs} +2 -2
  26. package/dist/{openapi-DhcCtKzM.cjs.map → openapi-D7WwlpPF.cjs.map} +1 -1
  27. package/dist/openapi.cjs +3 -3
  28. package/dist/openapi.mjs +3 -3
  29. package/dist/workspace/index.cjs +1 -1
  30. package/dist/workspace/index.d.cts +1 -1
  31. package/dist/workspace/index.d.mts +1 -1
  32. package/dist/workspace/index.mjs +1 -1
  33. package/dist/{workspace-BDAhr6Kb.cjs → workspace-CaVW6j2q.cjs} +10 -1
  34. package/dist/{workspace-BDAhr6Kb.cjs.map → workspace-CaVW6j2q.cjs.map} +1 -1
  35. package/dist/{workspace-D_6ZCaR_.mjs → workspace-DLFRaDc-.mjs} +10 -1
  36. package/dist/{workspace-D_6ZCaR_.mjs.map → workspace-DLFRaDc-.mjs.map} +1 -1
  37. package/package.json +3 -3
  38. package/src/auth/credentials.ts +66 -0
  39. package/src/deploy/dns/hostinger-api.ts +258 -0
  40. package/src/deploy/dns/index.ts +399 -0
  41. package/src/deploy/dokploy-api.ts +175 -0
  42. package/src/deploy/index.ts +389 -240
  43. package/src/deploy/state.ts +146 -0
  44. package/src/workspace/types.ts +629 -47
  45. package/tsconfig.tsbuildinfo +1 -1
  46. package/dist/dokploy-api-Bdmk5ImW.cjs +0 -3
  47. package/dist/dokploy-api-BdxOMH_V.cjs.map +0 -1
  48. package/dist/dokploy-api-DWsqNjwP.mjs.map +0 -1
  49. package/dist/dokploy-api-tZSZaHd9.mjs +0 -3
  50. package/dist/index-CXa3odEw.d.mts.map +0 -1
  51. package/dist/index-E8Nu2Rxl.d.cts.map +0 -1
@@ -0,0 +1,399 @@
1
+ /**
2
+ * DNS orchestration for deployments
3
+ *
4
+ * Handles automatic DNS record creation for deployed applications.
5
+ */
6
+
7
+ import { lookup } from 'node:dns/promises';
8
+ import { getHostingerToken, storeHostingerToken } from '../../auth/credentials';
9
+ import type { DnsConfig, DnsProvider } from '../../workspace/types';
10
+ import { HostingerApi } from './hostinger-api';
11
+
12
+ const logger = console;
13
+
14
+ /**
15
+ * Required DNS record for an app
16
+ */
17
+ export interface RequiredDnsRecord {
18
+ /** Full hostname (e.g., 'api.joemoer.traflabs.io') */
19
+ hostname: string;
20
+ /** Subdomain part for the DNS provider (e.g., 'api.joemoer') */
21
+ subdomain: string;
22
+ /** Record type */
23
+ type: 'A' | 'CNAME';
24
+ /** Target value (IP or hostname) */
25
+ value: string;
26
+ /** App name */
27
+ appName: string;
28
+ /** Whether the record was created */
29
+ created?: boolean;
30
+ /** Whether the record already existed */
31
+ existed?: boolean;
32
+ /** Error if creation failed */
33
+ error?: string;
34
+ }
35
+
36
+ /**
37
+ * Result of DNS record creation
38
+ */
39
+ export interface DnsCreationResult {
40
+ records: RequiredDnsRecord[];
41
+ success: boolean;
42
+ serverIp: string;
43
+ }
44
+
45
+ /**
46
+ * Resolve IP address from a hostname
47
+ */
48
+ export async function resolveHostnameToIp(hostname: string): Promise<string> {
49
+ try {
50
+ const addresses = await lookup(hostname, { family: 4 });
51
+ return addresses.address;
52
+ } catch (error) {
53
+ throw new Error(
54
+ `Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : 'Unknown error'}`,
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Extract subdomain from full hostname relative to root domain
61
+ *
62
+ * @example
63
+ * extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
64
+ * extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
65
+ */
66
+ export function extractSubdomain(hostname: string, rootDomain: string): string {
67
+ if (!hostname.endsWith(rootDomain)) {
68
+ throw new Error(
69
+ `Hostname ${hostname} is not under root domain ${rootDomain}`,
70
+ );
71
+ }
72
+
73
+ const subdomain = hostname.slice(0, -(rootDomain.length + 1)); // +1 for the dot
74
+ return subdomain || '@'; // '@' represents the root domain itself
75
+ }
76
+
77
+ /**
78
+ * Generate required DNS records for a deployment
79
+ */
80
+ export function generateRequiredRecords(
81
+ appHostnames: Map<string, string>, // appName -> hostname
82
+ rootDomain: string,
83
+ serverIp: string,
84
+ ): RequiredDnsRecord[] {
85
+ const records: RequiredDnsRecord[] = [];
86
+
87
+ for (const [appName, hostname] of appHostnames) {
88
+ const subdomain = extractSubdomain(hostname, rootDomain);
89
+ records.push({
90
+ hostname,
91
+ subdomain,
92
+ type: 'A',
93
+ value: serverIp,
94
+ appName,
95
+ });
96
+ }
97
+
98
+ return records;
99
+ }
100
+
101
+ /**
102
+ * Print DNS records table
103
+ */
104
+ export function printDnsRecordsTable(
105
+ records: RequiredDnsRecord[],
106
+ rootDomain: string,
107
+ ): void {
108
+ logger.log('\n 📋 DNS Records for ' + rootDomain + ':');
109
+ logger.log(
110
+ ' ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐',
111
+ );
112
+ logger.log(
113
+ ' │ Subdomain │ Type │ Value │ Status │',
114
+ );
115
+ logger.log(
116
+ ' ├─────────────────────────────────────┼──────┼─────────────────┼────────┤',
117
+ );
118
+
119
+ for (const record of records) {
120
+ const subdomain = record.subdomain.padEnd(35);
121
+ const type = record.type.padEnd(4);
122
+ const value = record.value.padEnd(15);
123
+ let status: string;
124
+
125
+ if (record.error) {
126
+ status = '✗';
127
+ } else if (record.created) {
128
+ status = '✓ new';
129
+ } else if (record.existed) {
130
+ status = '✓';
131
+ } else {
132
+ status = '?';
133
+ }
134
+
135
+ logger.log(
136
+ ` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`,
137
+ );
138
+ }
139
+
140
+ logger.log(
141
+ ' └─────────────────────────────────────┴──────┴─────────────────┴────────┘',
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Print DNS records in a simple format for manual setup
147
+ */
148
+ export function printDnsRecordsSimple(
149
+ records: RequiredDnsRecord[],
150
+ rootDomain: string,
151
+ ): void {
152
+ logger.log('\n 📋 Required DNS Records:');
153
+ logger.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
154
+
155
+ for (const record of records) {
156
+ logger.log(` ${record.subdomain} → ${record.value} (A record)`);
157
+ }
158
+
159
+ logger.log('');
160
+ }
161
+
162
+ /**
163
+ * Prompt for input (reuse from deploy/index.ts pattern)
164
+ */
165
+ async function promptForToken(message: string): Promise<string> {
166
+ const { stdin, stdout } = await import('node:process');
167
+ const readline = await import('node:readline/promises');
168
+
169
+ if (!stdin.isTTY) {
170
+ throw new Error('Interactive input required for Hostinger token.');
171
+ }
172
+
173
+ // Hidden input for token
174
+ stdout.write(message);
175
+ return new Promise((resolve) => {
176
+ let value = '';
177
+ const onData = (char: Buffer) => {
178
+ const c = char.toString();
179
+ if (c === '\n' || c === '\r') {
180
+ stdin.setRawMode(false);
181
+ stdin.pause();
182
+ stdin.removeListener('data', onData);
183
+ stdout.write('\n');
184
+ resolve(value);
185
+ } else if (c === '\u0003') {
186
+ stdin.setRawMode(false);
187
+ stdin.pause();
188
+ stdout.write('\n');
189
+ process.exit(1);
190
+ } else if (c === '\u007F' || c === '\b') {
191
+ if (value.length > 0) value = value.slice(0, -1);
192
+ } else {
193
+ value += c;
194
+ }
195
+ };
196
+ stdin.setRawMode(true);
197
+ stdin.resume();
198
+ stdin.on('data', onData);
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Create DNS records using the configured provider
204
+ */
205
+ export async function createDnsRecords(
206
+ records: RequiredDnsRecord[],
207
+ dnsConfig: DnsConfig,
208
+ ): Promise<RequiredDnsRecord[]> {
209
+ const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
210
+
211
+ if (provider === 'manual') {
212
+ // Just mark all records as needing manual creation
213
+ return records.map((r) => ({ ...r, created: false, existed: false }));
214
+ }
215
+
216
+ if (provider === 'hostinger') {
217
+ return createHostingerRecords(records, rootDomain, ttl);
218
+ }
219
+
220
+ if (provider === 'cloudflare') {
221
+ logger.log(' ⚠ Cloudflare DNS integration not yet implemented');
222
+ return records.map((r) => ({
223
+ ...r,
224
+ error: 'Cloudflare not implemented',
225
+ }));
226
+ }
227
+
228
+ return records;
229
+ }
230
+
231
+ /**
232
+ * Create DNS records at Hostinger
233
+ */
234
+ async function createHostingerRecords(
235
+ records: RequiredDnsRecord[],
236
+ rootDomain: string,
237
+ ttl: number,
238
+ ): Promise<RequiredDnsRecord[]> {
239
+ // Get or prompt for Hostinger token
240
+ let token = await getHostingerToken();
241
+
242
+ if (!token) {
243
+ logger.log('\n 📋 Hostinger API token not found.');
244
+ logger.log(
245
+ ' Get your token from: https://hpanel.hostinger.com/profile/api\n',
246
+ );
247
+
248
+ try {
249
+ token = await promptForToken(' Hostinger API Token: ');
250
+ await storeHostingerToken(token);
251
+ logger.log(' ✓ Token saved');
252
+ } catch {
253
+ logger.log(' ⚠ Could not get token, skipping DNS creation');
254
+ return records.map((r) => ({
255
+ ...r,
256
+ error: 'No API token',
257
+ }));
258
+ }
259
+ }
260
+
261
+ const api = new HostingerApi(token);
262
+ const results: RequiredDnsRecord[] = [];
263
+
264
+ // Get existing records to check what already exists
265
+ let existingRecords: Awaited<ReturnType<typeof api.getRecords>> = [];
266
+ try {
267
+ existingRecords = await api.getRecords(rootDomain);
268
+ } catch (error) {
269
+ const message = error instanceof Error ? error.message : 'Unknown error';
270
+ logger.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
271
+ return records.map((r) => ({ ...r, error: message }));
272
+ }
273
+
274
+ // Process each record
275
+ for (const record of records) {
276
+ const existing = existingRecords.find(
277
+ (r) => r.name === record.subdomain && r.type === 'A',
278
+ );
279
+
280
+ if (existing) {
281
+ // Record already exists
282
+ results.push({
283
+ ...record,
284
+ existed: true,
285
+ created: false,
286
+ });
287
+ continue;
288
+ }
289
+
290
+ // Create the record
291
+ try {
292
+ await api.upsertRecords(rootDomain, [
293
+ {
294
+ name: record.subdomain,
295
+ type: 'A',
296
+ ttl,
297
+ records: [record.value],
298
+ },
299
+ ]);
300
+
301
+ results.push({
302
+ ...record,
303
+ created: true,
304
+ existed: false,
305
+ });
306
+ } catch (error) {
307
+ const message = error instanceof Error ? error.message : 'Unknown error';
308
+ results.push({
309
+ ...record,
310
+ error: message,
311
+ });
312
+ }
313
+ }
314
+
315
+ return results;
316
+ }
317
+
318
+ /**
319
+ * Main DNS orchestration function for deployments
320
+ */
321
+ export async function orchestrateDns(
322
+ appHostnames: Map<string, string>, // appName -> hostname
323
+ dnsConfig: DnsConfig | undefined,
324
+ dokployEndpoint: string,
325
+ ): Promise<DnsCreationResult | null> {
326
+ if (!dnsConfig) {
327
+ return null;
328
+ }
329
+
330
+ const { domain: rootDomain, autoCreate = true } = dnsConfig;
331
+
332
+ // Resolve Dokploy server IP from endpoint
333
+ logger.log('\n🌐 Setting up DNS records...');
334
+ let serverIp: string;
335
+
336
+ try {
337
+ const endpointUrl = new URL(dokployEndpoint);
338
+ serverIp = await resolveHostnameToIp(endpointUrl.hostname);
339
+ logger.log(` Server IP: ${serverIp} (from ${endpointUrl.hostname})`);
340
+ } catch (error) {
341
+ const message = error instanceof Error ? error.message : 'Unknown error';
342
+ logger.log(` ⚠ Failed to resolve server IP: ${message}`);
343
+ return null;
344
+ }
345
+
346
+ // Generate required records
347
+ const requiredRecords = generateRequiredRecords(
348
+ appHostnames,
349
+ rootDomain,
350
+ serverIp,
351
+ );
352
+
353
+ if (requiredRecords.length === 0) {
354
+ logger.log(' No DNS records needed');
355
+ return { records: [], success: true, serverIp };
356
+ }
357
+
358
+ // Create records if auto-create is enabled
359
+ let finalRecords: RequiredDnsRecord[];
360
+
361
+ if (autoCreate && dnsConfig.provider !== 'manual') {
362
+ logger.log(` Creating DNS records at ${dnsConfig.provider}...`);
363
+ finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
364
+
365
+ const created = finalRecords.filter((r) => r.created).length;
366
+ const existed = finalRecords.filter((r) => r.existed).length;
367
+ const failed = finalRecords.filter((r) => r.error).length;
368
+
369
+ if (created > 0) {
370
+ logger.log(` ✓ Created ${created} DNS record(s)`);
371
+ }
372
+ if (existed > 0) {
373
+ logger.log(` ✓ ${existed} record(s) already exist`);
374
+ }
375
+ if (failed > 0) {
376
+ logger.log(` ⚠ ${failed} record(s) failed`);
377
+ }
378
+ } else {
379
+ finalRecords = requiredRecords;
380
+ }
381
+
382
+ // Print summary table
383
+ printDnsRecordsTable(finalRecords, rootDomain);
384
+
385
+ // If manual mode or some failed, print simple instructions
386
+ const hasFailures = finalRecords.some((r) => r.error);
387
+ if (dnsConfig.provider === 'manual' || hasFailures) {
388
+ printDnsRecordsSimple(
389
+ finalRecords.filter((r) => !r.created && !r.existed),
390
+ rootDomain,
391
+ );
392
+ }
393
+
394
+ return {
395
+ records: finalRecords,
396
+ success: !hasFailures,
397
+ serverIp,
398
+ };
399
+ }
@@ -177,6 +177,34 @@ export class DokployApi {
177
177
  // Application endpoints
178
178
  // ============================================
179
179
 
180
+ /**
181
+ * List all applications in a project
182
+ */
183
+ async listApplications(projectId: string): Promise<DokployApplication[]> {
184
+ try {
185
+ return await this.get<DokployApplication[]>(
186
+ `application.all?projectId=${projectId}`,
187
+ );
188
+ } catch {
189
+ // Fallback: endpoint might not exist in older Dokploy versions
190
+ return [];
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Find an application by name in a project
196
+ */
197
+ async findApplicationByName(
198
+ projectId: string,
199
+ name: string,
200
+ ): Promise<DokployApplication | undefined> {
201
+ const applications = await this.listApplications(projectId);
202
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
203
+ return applications.find(
204
+ (app) => app.name === name || app.appName === normalizedName,
205
+ );
206
+ }
207
+
180
208
  /**
181
209
  * Create a new application
182
210
  */
@@ -193,6 +221,42 @@ export class DokployApi {
193
221
  });
194
222
  }
195
223
 
224
+ /**
225
+ * Find or create an application by name
226
+ */
227
+ async findOrCreateApplication(
228
+ name: string,
229
+ projectId: string,
230
+ environmentId: string,
231
+ ): Promise<{ application: DokployApplication; created: boolean }> {
232
+ const existing = await this.findApplicationByName(projectId, name);
233
+ if (existing) {
234
+ return { application: existing, created: false };
235
+ }
236
+ const application = await this.createApplication(
237
+ name,
238
+ projectId,
239
+ environmentId,
240
+ );
241
+ return { application, created: true };
242
+ }
243
+
244
+ /**
245
+ * Get an application by ID
246
+ */
247
+ async getApplication(
248
+ applicationId: string,
249
+ ): Promise<DokployApplication | null> {
250
+ try {
251
+ return await this.get<DokployApplication>(
252
+ `application.one?applicationId=${applicationId}`,
253
+ );
254
+ } catch {
255
+ // Application not found
256
+ return null;
257
+ }
258
+ }
259
+
196
260
  /**
197
261
  * Update an application
198
262
  */
@@ -317,6 +381,34 @@ export class DokployApi {
317
381
  // Postgres endpoints
318
382
  // ============================================
319
383
 
384
+ /**
385
+ * List all Postgres databases in a project
386
+ */
387
+ async listPostgres(projectId: string): Promise<DokployPostgres[]> {
388
+ try {
389
+ return await this.get<DokployPostgres[]>(
390
+ `postgres.all?projectId=${projectId}`,
391
+ );
392
+ } catch {
393
+ // Fallback: endpoint might not exist in older Dokploy versions
394
+ return [];
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Find a Postgres database by name in a project
400
+ */
401
+ async findPostgresByName(
402
+ projectId: string,
403
+ name: string,
404
+ ): Promise<DokployPostgres | undefined> {
405
+ const databases = await this.listPostgres(projectId);
406
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
407
+ return databases.find(
408
+ (db) => db.name === name || db.appName === normalizedName,
409
+ );
410
+ }
411
+
320
412
  /**
321
413
  * Create a new Postgres database
322
414
  */
@@ -347,6 +439,30 @@ export class DokployApi {
347
439
  });
348
440
  }
349
441
 
442
+ /**
443
+ * Find or create a Postgres database by name
444
+ */
445
+ async findOrCreatePostgres(
446
+ name: string,
447
+ projectId: string,
448
+ environmentId: string,
449
+ options?: {
450
+ databasePassword?: string;
451
+ },
452
+ ): Promise<{ postgres: DokployPostgres; created: boolean }> {
453
+ const existing = await this.findPostgresByName(projectId, name);
454
+ if (existing) {
455
+ return { postgres: existing, created: false };
456
+ }
457
+ const postgres = await this.createPostgres(
458
+ name,
459
+ projectId,
460
+ environmentId,
461
+ options,
462
+ );
463
+ return { postgres, created: true };
464
+ }
465
+
350
466
  /**
351
467
  * Get a Postgres database by ID
352
468
  */
@@ -392,6 +508,34 @@ export class DokployApi {
392
508
  // Redis endpoints
393
509
  // ============================================
394
510
 
511
+ /**
512
+ * List all Redis instances in a project
513
+ */
514
+ async listRedis(projectId: string): Promise<DokployRedis[]> {
515
+ try {
516
+ return await this.get<DokployRedis[]>(
517
+ `redis.all?projectId=${projectId}`,
518
+ );
519
+ } catch {
520
+ // Fallback: endpoint might not exist in older Dokploy versions
521
+ return [];
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Find a Redis instance by name in a project
527
+ */
528
+ async findRedisByName(
529
+ projectId: string,
530
+ name: string,
531
+ ): Promise<DokployRedis | undefined> {
532
+ const instances = await this.listRedis(projectId);
533
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
534
+ return instances.find(
535
+ (redis) => redis.name === name || redis.appName === normalizedName,
536
+ );
537
+ }
538
+
395
539
  /**
396
540
  * Create a new Redis instance
397
541
  */
@@ -418,6 +562,25 @@ export class DokployApi {
418
562
  });
419
563
  }
420
564
 
565
+ /**
566
+ * Find or create a Redis instance by name
567
+ */
568
+ async findOrCreateRedis(
569
+ name: string,
570
+ projectId: string,
571
+ environmentId: string,
572
+ options?: {
573
+ databasePassword?: string;
574
+ },
575
+ ): Promise<{ redis: DokployRedis; created: boolean }> {
576
+ const existing = await this.findRedisByName(projectId, name);
577
+ if (existing) {
578
+ return { redis: existing, created: false };
579
+ }
580
+ const redis = await this.createRedis(name, projectId, environmentId, options);
581
+ return { redis, created: true };
582
+ }
583
+
421
584
  /**
422
585
  * Get a Redis instance by ID
423
586
  */
@@ -508,6 +671,18 @@ export class DokployApi {
508
671
  );
509
672
  }
510
673
 
674
+ /**
675
+ * Validate a domain and trigger SSL certificate generation
676
+ *
677
+ * This should be called after DNS records are created and propagated.
678
+ * It triggers Let's Encrypt certificate generation for HTTPS domains.
679
+ *
680
+ * @param domainId - The domain ID to validate
681
+ */
682
+ async validateDomain(domainId: string): Promise<void> {
683
+ await this.post('domain.validateDomain', { domainId });
684
+ }
685
+
511
686
  /**
512
687
  * Auto-generate a domain name for an application
513
688
  */