@geekmidas/cli 1.5.0 → 1.5.1

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 (82) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{HostingerProvider-B9N-TKbp.mjs → HostingerProvider-402UdK89.mjs} +34 -1
  3. package/dist/HostingerProvider-402UdK89.mjs.map +1 -0
  4. package/dist/{HostingerProvider-DUV9-Tzg.cjs → HostingerProvider-BiXdHjiq.cjs} +34 -1
  5. package/dist/HostingerProvider-BiXdHjiq.cjs.map +1 -0
  6. package/dist/{Route53Provider-C8mS0zY6.mjs → Route53Provider-DbBo7Uz5.mjs} +53 -1
  7. package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
  8. package/dist/{Route53Provider-Bs7Arms9.cjs → Route53Provider-kfJ77LmL.cjs} +53 -1
  9. package/dist/Route53Provider-kfJ77LmL.cjs.map +1 -0
  10. package/dist/backup-provisioner-B5e-F6zX.cjs +164 -0
  11. package/dist/backup-provisioner-B5e-F6zX.cjs.map +1 -0
  12. package/dist/backup-provisioner-BIArpmTr.mjs +163 -0
  13. package/dist/backup-provisioner-BIArpmTr.mjs.map +1 -0
  14. package/dist/{config-ZQM1vBoz.cjs → config-BYn5yUt5.cjs} +2 -2
  15. package/dist/{config-ZQM1vBoz.cjs.map → config-BYn5yUt5.cjs.map} +1 -1
  16. package/dist/{config-DfCJ29PQ.mjs → config-dLNQIvDR.mjs} +2 -2
  17. package/dist/{config-DfCJ29PQ.mjs.map → config-dLNQIvDR.mjs.map} +1 -1
  18. package/dist/config.cjs +2 -2
  19. package/dist/config.d.cts +1 -1
  20. package/dist/config.d.mts +2 -2
  21. package/dist/config.mjs +2 -2
  22. package/dist/{dokploy-api-z0833e7r.mjs → dokploy-api-2ldYoN3i.mjs} +131 -1
  23. package/dist/dokploy-api-2ldYoN3i.mjs.map +1 -0
  24. package/dist/dokploy-api-C93pveuy.mjs +3 -0
  25. package/dist/dokploy-api-CbDh4o93.cjs +3 -0
  26. package/dist/{dokploy-api-CQvhV6Hd.cjs → dokploy-api-DLgvEQlr.cjs} +131 -1
  27. package/dist/dokploy-api-DLgvEQlr.cjs.map +1 -0
  28. package/dist/{index-B58qjyBd.d.cts → index-Ba21_lNt.d.cts} +131 -29
  29. package/dist/index-Ba21_lNt.d.cts.map +1 -0
  30. package/dist/{index-C0SpUT9Y.d.mts → index-Bj5VNxEL.d.mts} +132 -30
  31. package/dist/index-Bj5VNxEL.d.mts.map +1 -0
  32. package/dist/index.cjs +119 -25
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.mjs +119 -25
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/{openapi-BcSjLfWq.mjs → openapi-CMTyaIJJ.mjs} +2 -2
  37. package/dist/{openapi-BcSjLfWq.mjs.map → openapi-CMTyaIJJ.mjs.map} +1 -1
  38. package/dist/{openapi-D6Hcfov0.cjs → openapi-CqblwJZ4.cjs} +2 -2
  39. package/dist/{openapi-D6Hcfov0.cjs.map → openapi-CqblwJZ4.cjs.map} +1 -1
  40. package/dist/openapi.cjs +3 -3
  41. package/dist/openapi.d.mts +1 -1
  42. package/dist/openapi.mjs +3 -3
  43. package/dist/{types-B9UZ7fOG.d.mts → types-CZg5iUgD.d.mts} +1 -1
  44. package/dist/{types-B9UZ7fOG.d.mts.map → types-CZg5iUgD.d.mts.map} +1 -1
  45. package/dist/workspace/index.cjs +1 -1
  46. package/dist/workspace/index.d.cts +1 -1
  47. package/dist/workspace/index.d.mts +2 -2
  48. package/dist/workspace/index.mjs +1 -1
  49. package/dist/{workspace-2Do2YcGZ.cjs → workspace-DIMnYaYt.cjs} +16 -2
  50. package/dist/{workspace-2Do2YcGZ.cjs.map → workspace-DIMnYaYt.cjs.map} +1 -1
  51. package/dist/{workspace-BW2iU37P.mjs → workspace-Dy8k7Wru.mjs} +16 -2
  52. package/dist/{workspace-BW2iU37P.mjs.map → workspace-Dy8k7Wru.mjs.map} +1 -1
  53. package/examples/cron-example.ts +6 -6
  54. package/examples/function-example.ts +1 -1
  55. package/package.json +7 -5
  56. package/src/deploy/__tests__/backup-provisioner.spec.ts +428 -0
  57. package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
  58. package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
  59. package/src/deploy/__tests__/undeploy.spec.ts +758 -0
  60. package/src/deploy/backup-provisioner.ts +316 -0
  61. package/src/deploy/dns/DnsProvider.ts +39 -1
  62. package/src/deploy/dns/HostingerProvider.ts +74 -0
  63. package/src/deploy/dns/Route53Provider.ts +81 -0
  64. package/src/deploy/dns/index.ts +25 -0
  65. package/src/deploy/dokploy-api.ts +237 -0
  66. package/src/deploy/index.ts +71 -13
  67. package/src/deploy/state.ts +171 -0
  68. package/src/deploy/undeploy.ts +407 -0
  69. package/src/generators/FunctionGenerator.ts +1 -1
  70. package/src/init/versions.ts +2 -2
  71. package/src/workspace/schema.ts +26 -0
  72. package/src/workspace/types.ts +14 -37
  73. package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
  74. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
  75. package/dist/Route53Provider-Bs7Arms9.cjs.map +0 -1
  76. package/dist/Route53Provider-C8mS0zY6.mjs.map +0 -1
  77. package/dist/dokploy-api-CQvhV6Hd.cjs.map +0 -1
  78. package/dist/dokploy-api-CWc02yyg.cjs +0 -3
  79. package/dist/dokploy-api-DSJYNx88.mjs +0 -3
  80. package/dist/dokploy-api-z0833e7r.mjs.map +0 -1
  81. package/dist/index-B58qjyBd.d.cts.map +0 -1
  82. package/dist/index-C0SpUT9Y.d.mts.map +0 -1
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Undeploy - Remove deployed resources from Dokploy
3
+ *
4
+ * Deletes applications, services (postgres, redis), DNS records, and optionally the project.
5
+ * Also handles cleanup of backup resources if configured.
6
+ */
7
+
8
+ import type { DnsProvider } from './dns/DnsProvider.js';
9
+ import type { DokployApi } from './dokploy-api.js';
10
+ import type { BackupState, DokployStageState } from './state.js';
11
+ import { getAllDnsRecords } from './state.js';
12
+
13
+ export interface UndeployOptions {
14
+ /** Dokploy API client */
15
+ api: DokployApi;
16
+ /** Deploy state for the stage */
17
+ state: DokployStageState;
18
+ /** DNS provider for deleting DNS records (optional) */
19
+ dnsProvider?: DnsProvider;
20
+ /** Whether to delete the Dokploy project (default: false) */
21
+ deleteProject?: boolean;
22
+ /** Whether to delete backup resources (S3 bucket, IAM user) - default: false */
23
+ deleteBackups?: boolean;
24
+ /** AWS endpoint override (for testing with LocalStack) */
25
+ awsEndpoint?: string;
26
+ /** Logger for progress output */
27
+ logger: { log: (msg: string) => void };
28
+ }
29
+
30
+ export interface UndeployResult {
31
+ /** Applications that were deleted */
32
+ deletedApplications: string[];
33
+ /** Whether postgres was deleted */
34
+ deletedPostgres: boolean;
35
+ /** Whether redis was deleted */
36
+ deletedRedis: boolean;
37
+ /** Whether the project was deleted */
38
+ deletedProject: boolean;
39
+ /** Whether backup destination was deleted */
40
+ deletedBackupDestination: boolean;
41
+ /** Whether AWS backup resources were deleted */
42
+ deletedAwsBackupResources: boolean;
43
+ /** DNS records that were deleted (name:type format) */
44
+ deletedDnsRecords: string[];
45
+ /** Updated state after undeploy (with deleted resources removed) */
46
+ updatedState: DokployStageState;
47
+ /** Errors encountered during undeploy (non-fatal) */
48
+ errors: string[];
49
+ }
50
+
51
+ /**
52
+ * Undeploy resources from Dokploy
53
+ *
54
+ * Executes in order:
55
+ * 1. Run final backup (if backup is configured)
56
+ * 2. Delete DNS records (if DNS provider is available)
57
+ * 3. Delete backup schedules
58
+ * 4. Delete applications
59
+ * 5. Delete postgres database
60
+ * 6. Delete redis instance
61
+ * 7. Delete backup destination
62
+ * 8. Delete AWS backup resources (if deleteBackups is true)
63
+ * 9. Delete project (if deleteProject is true)
64
+ *
65
+ * Returns the updated state with deleted resources removed.
66
+ */
67
+ export async function undeploy(
68
+ options: UndeployOptions,
69
+ ): Promise<UndeployResult> {
70
+ const {
71
+ api,
72
+ state,
73
+ dnsProvider,
74
+ deleteProject = false,
75
+ deleteBackups = false,
76
+ awsEndpoint,
77
+ logger,
78
+ } = options;
79
+
80
+ // Create a mutable copy of the state to track deletions
81
+ const updatedState: DokployStageState = JSON.parse(JSON.stringify(state));
82
+
83
+ const result: UndeployResult = {
84
+ deletedApplications: [],
85
+ deletedPostgres: false,
86
+ deletedRedis: false,
87
+ deletedProject: false,
88
+ deletedBackupDestination: false,
89
+ deletedAwsBackupResources: false,
90
+ deletedDnsRecords: [],
91
+ updatedState,
92
+ errors: [],
93
+ };
94
+
95
+ // 1. Run a final backup before undeploying (if backup is configured)
96
+ if (state.backups?.postgresBackupId) {
97
+ try {
98
+ logger.log(' Running final postgres backup before undeploy...');
99
+ await api.runBackupManually(state.backups.postgresBackupId);
100
+ logger.log(' ✓ Final backup triggered');
101
+ } catch (error) {
102
+ const msg = `Failed to run final backup: ${error}`;
103
+ logger.log(` ⚠ ${msg}`);
104
+ result.errors.push(msg);
105
+ }
106
+ }
107
+
108
+ // 2. Delete DNS records (if DNS provider is available)
109
+ if (dnsProvider) {
110
+ const dnsRecords = getAllDnsRecords(state);
111
+ if (dnsRecords.length > 0) {
112
+ // Group records by domain
113
+ const recordsByDomain = new Map<
114
+ string,
115
+ Array<{ name: string; type: string }>
116
+ >();
117
+ for (const record of dnsRecords) {
118
+ const existing = recordsByDomain.get(record.domain) ?? [];
119
+ existing.push({ name: record.name, type: record.type });
120
+ recordsByDomain.set(record.domain, existing);
121
+ }
122
+
123
+ for (const [domain, records] of recordsByDomain) {
124
+ try {
125
+ logger.log(
126
+ ` Deleting ${records.length} DNS record(s) for ${domain}...`,
127
+ );
128
+ const deleteResults = await dnsProvider.deleteRecords(
129
+ domain,
130
+ records.map((r) => ({
131
+ name: r.name,
132
+ type: r.type as
133
+ | 'A'
134
+ | 'AAAA'
135
+ | 'CNAME'
136
+ | 'MX'
137
+ | 'TXT'
138
+ | 'SRV'
139
+ | 'CAA',
140
+ })),
141
+ );
142
+
143
+ for (const deleteResult of deleteResults) {
144
+ const key = `${deleteResult.record.name}:${deleteResult.record.type}`;
145
+ if (deleteResult.deleted || deleteResult.notFound) {
146
+ result.deletedDnsRecords.push(key);
147
+ // Remove from state
148
+ if (updatedState.dnsRecords) {
149
+ delete updatedState.dnsRecords[key];
150
+ }
151
+ if (updatedState.dnsVerified) {
152
+ // Find and remove hostname from dnsVerified
153
+ const hostname =
154
+ deleteResult.record.name === '@'
155
+ ? domain
156
+ : `${deleteResult.record.name}.${domain}`;
157
+ delete updatedState.dnsVerified[hostname];
158
+ }
159
+ logger.log(` ✓ DNS record ${key} deleted`);
160
+ } else if (deleteResult.error) {
161
+ const msg = `Failed to delete DNS record ${key}: ${deleteResult.error}`;
162
+ logger.log(` ⚠ ${msg}`);
163
+ result.errors.push(msg);
164
+ }
165
+ }
166
+ } catch (error) {
167
+ const msg = `Failed to delete DNS records for ${domain}: ${error}`;
168
+ logger.log(` ⚠ ${msg}`);
169
+ result.errors.push(msg);
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ // 3. Delete backup schedules (before deleting postgres)
176
+ if (state.backups?.postgresBackupId) {
177
+ try {
178
+ logger.log(' Deleting postgres backup schedule...');
179
+ await api.deleteBackup(state.backups.postgresBackupId);
180
+ if (updatedState.backups) {
181
+ delete updatedState.backups.postgresBackupId;
182
+ }
183
+ logger.log(' ✓ Backup schedule deleted');
184
+ } catch (error) {
185
+ const msg = `Failed to delete backup schedule: ${error}`;
186
+ logger.log(` ⚠ ${msg}`);
187
+ result.errors.push(msg);
188
+ }
189
+ }
190
+
191
+ // 4. Delete all applications
192
+ for (const [appName, applicationId] of Object.entries(state.applications)) {
193
+ try {
194
+ logger.log(` Deleting application: ${appName}...`);
195
+ await api.deleteApplication(applicationId);
196
+ result.deletedApplications.push(appName);
197
+ delete updatedState.applications[appName];
198
+ // Also remove app credentials and generated secrets
199
+ if (updatedState.appCredentials) {
200
+ delete updatedState.appCredentials[appName];
201
+ }
202
+ if (updatedState.generatedSecrets) {
203
+ delete updatedState.generatedSecrets[appName];
204
+ }
205
+ logger.log(` ✓ Application ${appName} deleted`);
206
+ } catch (error) {
207
+ const msg = `Failed to delete application ${appName}: ${error}`;
208
+ logger.log(` ⚠ ${msg}`);
209
+ result.errors.push(msg);
210
+ }
211
+ }
212
+
213
+ // 5. Delete postgres if exists
214
+ if (state.services.postgresId) {
215
+ try {
216
+ logger.log(' Deleting postgres database...');
217
+ await api.deletePostgres(state.services.postgresId);
218
+ result.deletedPostgres = true;
219
+ delete updatedState.services.postgresId;
220
+ logger.log(' ✓ Postgres deleted');
221
+ } catch (error) {
222
+ const msg = `Failed to delete postgres: ${error}`;
223
+ logger.log(` ⚠ ${msg}`);
224
+ result.errors.push(msg);
225
+ }
226
+ }
227
+
228
+ // 6. Delete redis if exists
229
+ if (state.services.redisId) {
230
+ try {
231
+ logger.log(' Deleting redis instance...');
232
+ await api.deleteRedis(state.services.redisId);
233
+ result.deletedRedis = true;
234
+ delete updatedState.services.redisId;
235
+ logger.log(' ✓ Redis deleted');
236
+ } catch (error) {
237
+ const msg = `Failed to delete redis: ${error}`;
238
+ logger.log(` ⚠ ${msg}`);
239
+ result.errors.push(msg);
240
+ }
241
+ }
242
+
243
+ // 7. Delete backup destination from Dokploy
244
+ if (state.backups?.destinationId) {
245
+ try {
246
+ logger.log(' Deleting backup destination...');
247
+ await api.deleteDestination(state.backups.destinationId);
248
+ result.deletedBackupDestination = true;
249
+ logger.log(' ✓ Backup destination deleted');
250
+ } catch (error) {
251
+ const msg = `Failed to delete backup destination: ${error}`;
252
+ logger.log(` ⚠ ${msg}`);
253
+ result.errors.push(msg);
254
+ }
255
+ }
256
+
257
+ // 8. Delete AWS backup resources if requested
258
+ if (deleteBackups && state.backups) {
259
+ try {
260
+ logger.log(' Deleting AWS backup resources...');
261
+ await deleteAwsBackupResources(state.backups, awsEndpoint, logger);
262
+ result.deletedAwsBackupResources = true;
263
+ // Clear backup state entirely
264
+ delete updatedState.backups;
265
+ logger.log(' ✓ AWS backup resources deleted');
266
+ } catch (error) {
267
+ const msg = `Failed to delete AWS backup resources: ${error}`;
268
+ logger.log(` ⚠ ${msg}`);
269
+ result.errors.push(msg);
270
+ }
271
+ }
272
+
273
+ // 9. Delete project if requested
274
+ if (deleteProject) {
275
+ try {
276
+ logger.log(' Deleting Dokploy project...');
277
+ await api.deleteProject(state.projectId);
278
+ result.deletedProject = true;
279
+ logger.log(' ✓ Project deleted');
280
+ } catch (error) {
281
+ const msg = `Failed to delete project: ${error}`;
282
+ logger.log(` ⚠ ${msg}`);
283
+ result.errors.push(msg);
284
+ }
285
+ }
286
+
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * Delete AWS backup resources (S3 bucket, IAM user)
292
+ */
293
+ async function deleteAwsBackupResources(
294
+ backupState: BackupState,
295
+ awsEndpoint?: string,
296
+ logger?: { log: (msg: string) => void },
297
+ ): Promise<void> {
298
+ const {
299
+ S3Client,
300
+ DeleteBucketCommand,
301
+ DeleteObjectsCommand,
302
+ ListObjectsV2Command,
303
+ } = await import('@aws-sdk/client-s3');
304
+
305
+ const {
306
+ IAMClient,
307
+ DeleteAccessKeyCommand,
308
+ DeleteUserCommand,
309
+ DeleteUserPolicyCommand,
310
+ ListAccessKeysCommand,
311
+ } = await import('@aws-sdk/client-iam');
312
+
313
+ const clientConfig: {
314
+ region: string;
315
+ endpoint?: string;
316
+ forcePathStyle?: boolean;
317
+ credentials?: { accessKeyId: string; secretAccessKey: string };
318
+ } = {
319
+ region: backupState.region,
320
+ };
321
+
322
+ if (awsEndpoint) {
323
+ clientConfig.endpoint = awsEndpoint;
324
+ clientConfig.forcePathStyle = true;
325
+ clientConfig.credentials = {
326
+ accessKeyId: 'test',
327
+ secretAccessKey: 'test',
328
+ };
329
+ }
330
+
331
+ const s3 = new S3Client(clientConfig);
332
+ const iam = new IAMClient(clientConfig);
333
+
334
+ try {
335
+ // Delete all objects in the bucket first
336
+ logger?.log(` Emptying bucket: ${backupState.bucketName}`);
337
+ let continuationToken: string | undefined;
338
+ do {
339
+ const listResult = await s3.send(
340
+ new ListObjectsV2Command({
341
+ Bucket: backupState.bucketName,
342
+ ContinuationToken: continuationToken,
343
+ }),
344
+ );
345
+
346
+ if (listResult.Contents?.length) {
347
+ await s3.send(
348
+ new DeleteObjectsCommand({
349
+ Bucket: backupState.bucketName,
350
+ Delete: {
351
+ Objects: listResult.Contents.map((o) => ({ Key: o.Key })),
352
+ },
353
+ }),
354
+ );
355
+ }
356
+
357
+ continuationToken = listResult.NextContinuationToken;
358
+ } while (continuationToken);
359
+
360
+ // Delete the bucket
361
+ logger?.log(` Deleting bucket: ${backupState.bucketName}`);
362
+ await s3.send(new DeleteBucketCommand({ Bucket: backupState.bucketName }));
363
+ } catch (error) {
364
+ // Bucket might not exist, continue with IAM cleanup
365
+ logger?.log(` Warning: Could not delete bucket: ${error}`);
366
+ }
367
+
368
+ try {
369
+ // Delete all access keys for the IAM user
370
+ logger?.log(
371
+ ` Deleting IAM access keys for: ${backupState.iamUserName}`,
372
+ );
373
+ const keysResult = await iam.send(
374
+ new ListAccessKeysCommand({ UserName: backupState.iamUserName }),
375
+ );
376
+
377
+ for (const key of keysResult.AccessKeyMetadata ?? []) {
378
+ await iam.send(
379
+ new DeleteAccessKeyCommand({
380
+ UserName: backupState.iamUserName,
381
+ AccessKeyId: key.AccessKeyId,
382
+ }),
383
+ );
384
+ }
385
+
386
+ // Delete the user policy
387
+ logger?.log(` Deleting IAM policy for: ${backupState.iamUserName}`);
388
+ await iam.send(
389
+ new DeleteUserPolicyCommand({
390
+ UserName: backupState.iamUserName,
391
+ PolicyName: 'DokployBackupAccess',
392
+ }),
393
+ );
394
+
395
+ // Delete the IAM user
396
+ logger?.log(` Deleting IAM user: ${backupState.iamUserName}`);
397
+ await iam.send(
398
+ new DeleteUserCommand({ UserName: backupState.iamUserName }),
399
+ );
400
+ } catch (error) {
401
+ // IAM user might not exist
402
+ logger?.log(` Warning: Could not delete IAM user: ${error}`);
403
+ }
404
+
405
+ s3.destroy();
406
+ iam.destroy();
407
+ }
@@ -99,7 +99,7 @@ export class FunctionGenerator extends ConstructGenerator<
99
99
  context.loggerPath,
100
100
  );
101
101
 
102
- const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/functions';
102
+ const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
103
103
  import { ${exportName} } from '${importPath}';
104
104
  import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
105
105
  import ${context.loggerImportPattern} from '${relativeLoggerPath}';
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
32
32
  '@geekmidas/cache': '~1.0.0',
33
33
  '@geekmidas/client': '~1.0.0',
34
34
  '@geekmidas/cloud': '~1.0.0',
35
- '@geekmidas/constructs': '~1.0.0',
35
+ '@geekmidas/constructs': '~1.0.4',
36
36
  '@geekmidas/db': '~1.0.0',
37
37
  '@geekmidas/emailkit': '~1.0.0',
38
- '@geekmidas/envkit': '~1.0.0',
38
+ '@geekmidas/envkit': '~1.0.1',
39
39
  '@geekmidas/errors': '~1.0.0',
40
40
  '@geekmidas/events': '~1.0.0',
41
41
  '@geekmidas/logger': '~1.0.0',
@@ -360,6 +360,8 @@ export const DnsProviderSchema = z.union([
360
360
  CustomDnsProviderSchema,
361
361
  ]);
362
362
 
363
+ export type DnsProvider = z.infer<typeof DnsProviderSchema>;
364
+
363
365
  /**
364
366
  * DNS configuration schema.
365
367
  *
@@ -416,6 +418,29 @@ export const DnsConfigWithLegacySchema = z.union([
416
418
  LegacyDnsConfigSchema,
417
419
  ]);
418
420
 
421
+ export type DnsConfig = z.infer<typeof DnsConfigWithLegacySchema>;
422
+
423
+ /**
424
+ * Backups configuration schema.
425
+ *
426
+ * Configures automatic backup destinations for database services.
427
+ * On first deploy, creates S3 bucket with unique name and IAM credentials.
428
+ */
429
+ export const BackupsConfigSchema = z.object({
430
+ /** Backup storage type (currently only 's3' supported) */
431
+ type: z.literal('s3'),
432
+ /** AWS profile name for creating bucket/IAM resources */
433
+ profile: z.string().optional(),
434
+ /** AWS region for the backup bucket */
435
+ region: AwsRegionSchema,
436
+ /** Cron schedule for backups (default: '0 2 * * *' = 2 AM daily) */
437
+ schedule: z.string().optional(),
438
+ /** Number of backups to retain (default: 30) */
439
+ retention: z.number().optional(),
440
+ });
441
+
442
+ export type BackupsConfig = z.infer<typeof BackupsConfigSchema>;
443
+
419
444
  /**
420
445
  * Deploy configuration schema.
421
446
  */
@@ -423,6 +448,7 @@ const DeployConfigSchema = z.object({
423
448
  default: DeployTargetSchema.optional(),
424
449
  dokploy: DokployWorkspaceConfigSchema.optional(),
425
450
  dns: DnsConfigWithLegacySchema.optional(),
451
+ backups: BackupsConfigSchema.optional(),
426
452
  });
427
453
 
428
454
  /**
@@ -1,5 +1,7 @@
1
- import type { z } from 'zod/v4';
2
- import type { StateConfig } from '../deploy/StateProvider.js';
1
+ import type { AwsRegion, StateConfig } from '../deploy/StateProvider.js';
2
+
3
+ export type { AwsRegion };
4
+
3
5
  import type {
4
6
  GkmConfig,
5
7
  HooksConfig,
@@ -10,7 +12,9 @@ import type {
10
12
  StudioConfig,
11
13
  TelescopeConfig,
12
14
  } from '../types.js';
13
- import type { DnsConfigWithLegacySchema, DnsProviderSchema } from './schema.js';
15
+ import type { BackupsConfig, DnsConfig, DnsProvider } from './schema.js';
16
+
17
+ export type { BackupsConfig, DnsConfig, DnsProvider };
14
18
 
15
19
  /**
16
20
  * Deploy target for an app.
@@ -263,39 +267,6 @@ export interface DokployWorkspaceConfig {
263
267
  */
264
268
  export type DnsProviderType = 'hostinger' | 'route53' | 'cloudflare' | 'manual';
265
269
 
266
- /**
267
- * DNS provider configuration for a single domain.
268
- */
269
- export type DnsProvider = z.infer<typeof DnsProviderSchema>;
270
-
271
- /**
272
- * DNS configuration for automatic record creation during deployment.
273
- *
274
- * Maps root domains to their DNS provider configuration.
275
- * When configured, the deploy command will automatically create DNS
276
- * A records pointing to your Dokploy server for each app's domain.
277
- *
278
- * @example
279
- * ```ts
280
- * // Multi-domain with different providers
281
- * dns: {
282
- * 'geekmidas.dev': { provider: 'hostinger' },
283
- * 'geekmidas.com': { provider: 'route53' },
284
- * }
285
- *
286
- * // Single domain
287
- * dns: {
288
- * 'traflabs.io': { provider: 'hostinger', ttl: 300 },
289
- * }
290
- *
291
- * // Manual mode - just print required records
292
- * dns: {
293
- * 'myapp.com': { provider: 'manual' },
294
- * }
295
- * ```
296
- */
297
- export type DnsConfig = z.infer<typeof DnsConfigWithLegacySchema>;
298
-
299
270
  /**
300
271
  * Deployment configuration for the workspace.
301
272
  *
@@ -306,7 +277,7 @@ export type DnsConfig = z.infer<typeof DnsConfigWithLegacySchema>;
306
277
  * default: 'dokploy',
307
278
  * }
308
279
  *
309
- * // Full configuration with DNS
280
+ * // Full configuration with DNS and backups
310
281
  * deploy: {
311
282
  * default: 'dokploy',
312
283
  * dokploy: {
@@ -321,6 +292,10 @@ export type DnsConfig = z.infer<typeof DnsConfigWithLegacySchema>;
321
292
  * provider: 'hostinger',
322
293
  * domain: 'myapp.com',
323
294
  * },
295
+ * backups: {
296
+ * type: 's3',
297
+ * region: 'us-east-1',
298
+ * },
324
299
  * }
325
300
  * ```
326
301
  */
@@ -331,6 +306,8 @@ export interface DeployConfig {
331
306
  dokploy?: DokployWorkspaceConfig;
332
307
  /** DNS configuration for automatic record creation */
333
308
  dns?: DnsConfig;
309
+ /** Backup destination configuration for database services */
310
+ backups?: BackupsConfig;
334
311
  }
335
312
 
336
313
  /**
@@ -1 +0,0 @@
1
- {"version":3,"file":"HostingerProvider-B9N-TKbp.mjs","names":["message: string","status: number","statusText: string","errors?: Record<string, string[]>","token: string","method: 'GET' | 'POST' | 'PUT' | 'DELETE'","endpoint: string","body?: unknown","errors: Record<string, string[]> | undefined","domain: string","records: DnsRecord[]","filters: DnsRecordFilter[]","name: string","type: DnsRecordType","subdomain: string","ip: string","domain: string","records: UpsertDnsRecord[]","results: UpsertResult[]"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tinterface RecordResponse {\n\t\t\tdata: Array<{\n\t\t\t\tname: string;\n\t\t\t\ttype: DnsRecordType;\n\t\t\t\tttl: number;\n\t\t\t\trecords: Array<{ content: string }>;\n\t\t\t}>;\n\t\t}\n\n\t\tconst response = await this.request<RecordResponse>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\treturn response.data || [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"HostingerProvider-DUV9-Tzg.cjs","names":["message: string","status: number","statusText: string","errors?: Record<string, string[]>","token: string","method: 'GET' | 'POST' | 'PUT' | 'DELETE'","endpoint: string","body?: unknown","errors: Record<string, string[]> | undefined","domain: string","records: DnsRecord[]","filters: DnsRecordFilter[]","name: string","type: DnsRecordType","subdomain: string","ip: string","domain: string","records: UpsertDnsRecord[]","results: UpsertResult[]"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tinterface RecordResponse {\n\t\t\tdata: Array<{\n\t\t\t\tname: string;\n\t\t\t\ttype: DnsRecordType;\n\t\t\t\tttl: number;\n\t\t\t\trecords: Array<{ content: string }>;\n\t\t\t}>;\n\t\t}\n\n\t\tconst response = await this.request<RecordResponse>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\treturn response.data || [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Route53Provider-Bs7Arms9.cjs","names":["options: Route53ProviderOptions","Route53Client","domain: string","ListHostedZonesByNameCommand","type: string","validTypes: DnsRecordType[]","recordName: string","records: DnsRecord[]","nextRecordName: string | undefined","nextRecordType: RRType | undefined","ListResourceRecordSetsCommand","records: UpsertDnsRecord[]","results: UpsertResult[]","ChangeResourceRecordSetsCommand"],"sources":["../src/deploy/dns/Route53Provider.ts"],"sourcesContent":["/**\n * Route53 DNS Provider\n *\n * Implements DnsProvider interface using AWS Route53.\n */\n\nimport {\n\tChangeResourceRecordSetsCommand,\n\tListHostedZonesByNameCommand,\n\tListResourceRecordSetsCommand,\n\tRoute53Client,\n\ttype RRType,\n} from '@aws-sdk/client-route-53';\nimport { fromIni } from '@aws-sdk/credential-providers';\nimport type {\n\tDnsProvider,\n\tDnsRecord,\n\tDnsRecordType,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\n\nexport interface Route53ProviderOptions {\n\t/** AWS region (optional - uses AWS_REGION env var if not provided) */\n\tregion?: string;\n\t/** AWS profile name (optional - uses default credential chain if not provided) */\n\tprofile?: string;\n\t/** Hosted zone ID (optional - auto-detected from domain if not provided) */\n\thostedZoneId?: string;\n\t/** Custom endpoint for testing with localstack */\n\tendpoint?: string;\n}\n\n/**\n * Route53 DNS provider implementation.\n *\n * Uses AWS default credential chain for authentication.\n * Region can be specified or will use AWS_REGION/AWS_DEFAULT_REGION env vars.\n * Profile can be specified to use a named profile from ~/.aws/credentials.\n */\nexport class Route53Provider implements DnsProvider {\n\treadonly name = 'route53';\n\tprivate client: Route53Client;\n\tprivate hostedZoneId?: string;\n\tprivate hostedZoneCache: Map<string, string> = new Map();\n\n\tconstructor(options: Route53ProviderOptions = {}) {\n\t\t// Route53 is a global service, default to us-east-1 if no region specified\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\n\t\tthis.client = new Route53Client({\n\t\t\tregion,\n\t\t\t...(options.endpoint && { endpoint: options.endpoint }),\n\t\t\t...(options.profile && {\n\t\t\t\tcredentials: fromIni({ profile: options.profile }),\n\t\t\t}),\n\t\t});\n\t\tthis.hostedZoneId = options.hostedZoneId;\n\t}\n\n\t/**\n\t * Get the hosted zone ID for a domain.\n\t * Uses cache to avoid repeated API calls.\n\t */\n\tprivate async getHostedZoneId(domain: string): Promise<string> {\n\t\t// Use configured zone ID if provided\n\t\tif (this.hostedZoneId) {\n\t\t\treturn this.hostedZoneId;\n\t\t}\n\n\t\t// Check cache\n\t\tif (this.hostedZoneCache.has(domain)) {\n\t\t\treturn this.hostedZoneCache.get(domain)!;\n\t\t}\n\n\t\t// Auto-detect from domain\n\t\tconst command = new ListHostedZonesByNameCommand({\n\t\t\tDNSName: domain,\n\t\t\tMaxItems: 1,\n\t\t});\n\n\t\tconst response = await this.client.send(command);\n\t\tconst zones = response.HostedZones ?? [];\n\n\t\t// Find exact match (domain with trailing dot)\n\t\tconst normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;\n\t\tconst zone = zones.find((z) => z.Name === normalizedDomain);\n\n\t\tif (!zone?.Id) {\n\t\t\tthrow new Error(\n\t\t\t\t`No hosted zone found for domain: ${domain}. Create one in Route53 or provide hostedZoneId in config.`,\n\t\t\t);\n\t\t}\n\n\t\t// Zone ID comes as \"/hostedzone/Z1234567890\" - extract just the ID\n\t\tconst zoneId = zone.Id.replace('/hostedzone/', '');\n\t\tthis.hostedZoneCache.set(domain, zoneId);\n\t\treturn zoneId;\n\t}\n\n\t/**\n\t * Convert Route53 record type to our DnsRecordType.\n\t * Excludes NS and SOA which are auto-managed by Route53 for the zone.\n\t */\n\tprivate toRecordType(type: string): DnsRecordType | null {\n\t\t// Exclude NS and SOA which are auto-managed zone records\n\t\tconst managedTypes = ['NS', 'SOA'];\n\t\tif (managedTypes.includes(type)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst validTypes: DnsRecordType[] = [\n\t\t\t'A',\n\t\t\t'AAAA',\n\t\t\t'CNAME',\n\t\t\t'MX',\n\t\t\t'TXT',\n\t\t\t'SRV',\n\t\t\t'CAA',\n\t\t];\n\t\treturn validTypes.includes(type as DnsRecordType)\n\t\t\t? (type as DnsRecordType)\n\t\t\t: null;\n\t}\n\n\t/**\n\t * Extract subdomain from full record name relative to domain.\n\t */\n\tprivate extractSubdomain(recordName: string, domain: string): string {\n\t\tconst normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;\n\t\tconst normalizedName = recordName.endsWith('.')\n\t\t\t? recordName\n\t\t\t: `${recordName}.`;\n\n\t\tif (normalizedName === normalizedDomain) {\n\t\t\treturn '@';\n\t\t}\n\n\t\t// Remove the domain suffix\n\t\tconst subdomain = normalizedName.replace(`.${normalizedDomain}`, '');\n\t\treturn subdomain.replace(/\\.$/, ''); // Remove trailing dot if any\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst records: DnsRecord[] = [];\n\n\t\tlet nextRecordName: string | undefined;\n\t\tlet nextRecordType: RRType | undefined;\n\n\t\t// Paginate through all records\n\t\tdo {\n\t\t\tconst command = new ListResourceRecordSetsCommand({\n\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\tStartRecordName: nextRecordName,\n\t\t\t\tStartRecordType: nextRecordType,\n\t\t\t\tMaxItems: 100,\n\t\t\t});\n\n\t\t\tconst response = await this.client.send(command);\n\n\t\t\tfor (const recordSet of response.ResourceRecordSets ?? []) {\n\t\t\t\tconst type = this.toRecordType(recordSet.Type ?? '');\n\t\t\t\tif (!type || !recordSet.Name) continue;\n\n\t\t\t\tconst values = (recordSet.ResourceRecords ?? [])\n\t\t\t\t\t.map((r) => r.Value)\n\t\t\t\t\t.filter((v): v is string => !!v);\n\n\t\t\t\trecords.push({\n\t\t\t\t\tname: this.extractSubdomain(recordSet.Name, domain),\n\t\t\t\t\ttype,\n\t\t\t\t\tttl: recordSet.TTL ?? 300,\n\t\t\t\t\tvalues,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (response.IsTruncated) {\n\t\t\t\tnextRecordName = response.NextRecordName;\n\t\t\t\tnextRecordType = response.NextRecordType;\n\t\t\t} else {\n\t\t\t\tnextRecordName = undefined;\n\t\t\t}\n\t\t} while (nextRecordName);\n\n\t\treturn records;\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to determine if creating or updating\n\t\tconst existingRecords = await this.getRecords(domain);\n\n\t\t// Process records in batches (Route53 allows max 1000 changes per request)\n\t\tconst batchSize = 100;\n\t\tfor (let i = 0; i < records.length; i += batchSize) {\n\t\t\tconst batch = records.slice(i, i + batchSize);\n\t\t\tconst changes = [];\n\n\t\t\tfor (const record of batch) {\n\t\t\t\tconst existing = existingRecords.find(\n\t\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t\t);\n\n\t\t\t\tconst existingValue = existing?.values?.[0];\n\n\t\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tcreated: false,\n\t\t\t\t\t\tunchanged: true,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Build full record name\n\t\t\t\tconst recordName =\n\t\t\t\t\trecord.name === '@' ? domain : `${record.name}.${domain}`;\n\n\t\t\t\tchanges.push({\n\t\t\t\t\tAction: 'UPSERT' as const,\n\t\t\t\t\tResourceRecordSet: {\n\t\t\t\t\t\tName: recordName,\n\t\t\t\t\t\tType: record.type,\n\t\t\t\t\t\tTTL: record.ttl,\n\t\t\t\t\t\tResourceRecords: [{ Value: record.value }],\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: !existing,\n\t\t\t\t\tunchanged: false,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Execute batch if there are changes\n\t\t\tif (changes.length > 0) {\n\t\t\t\tconst command = new ChangeResourceRecordSetsCommand({\n\t\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\t\tChangeBatch: {\n\t\t\t\t\t\tComment: 'Upsert by gkm deploy',\n\t\t\t\t\t\tChanges: changes,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tawait this.client.send(command);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}