@geekmidas/cli 0.54.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +26 -5
- package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
- package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
- package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
- package/dist/Route53Provider-CpRIqu69.cjs +157 -0
- package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
- package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
- package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
- package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
- package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
- package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
- package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
- package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
- package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
- package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
- package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/credentials-C8DWtnMY.cjs +174 -0
- package/dist/credentials-C8DWtnMY.cjs.map +1 -0
- package/dist/credentials-DT1dSxIx.mjs +126 -0
- package/dist/credentials-DT1dSxIx.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
- package/dist/dokploy-api-CHa8G51l.mjs +3 -0
- package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
- package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
- package/dist/dokploy-api-CWc02yyg.cjs +3 -0
- package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
- package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
- package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/encryption-UUmaWAmz.mjs +3 -0
- package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
- package/dist/index-B5rGIc4g.d.cts.map +1 -0
- package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
- package/dist/index-KFEbMIRa.d.mts.map +1 -0
- package/dist/index.cjs +2223 -606
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2200 -583
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
- package/dist/openapi-BMFmLnX6.mjs.map +1 -0
- package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
- package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
- package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
- package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +2 -2
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
- package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
- package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
- package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
- package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
- package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
- package/dist/workspace-BFRUOOrh.cjs.map +1 -0
- package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
- package/dist/workspace-DAxG3_H2.mjs.map +1 -0
- package/package.json +12 -8
- package/src/build/__tests__/handler-templates.spec.ts +115 -47
- package/src/deploy/CachedStateProvider.ts +86 -0
- package/src/deploy/LocalStateProvider.ts +57 -0
- package/src/deploy/SSMStateProvider.ts +93 -0
- package/src/deploy/StateProvider.ts +171 -0
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
- package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
- package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
- package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
- package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
- package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
- package/src/deploy/__tests__/sniffer.spec.ts +4 -20
- package/src/deploy/__tests__/state.spec.ts +13 -5
- package/src/deploy/dns/DnsProvider.ts +163 -0
- package/src/deploy/dns/HostingerProvider.ts +100 -0
- package/src/deploy/dns/Route53Provider.ts +256 -0
- package/src/deploy/dns/index.ts +257 -165
- package/src/deploy/env-resolver.ts +12 -5
- package/src/deploy/index.ts +16 -13
- package/src/deploy/sniffer-envkit-patch.ts +3 -1
- package/src/deploy/sniffer-routes-worker.ts +104 -0
- package/src/deploy/sniffer.ts +77 -55
- package/src/deploy/state-commands.ts +274 -0
- package/src/dev/__tests__/entry.spec.ts +8 -2
- package/src/dev/__tests__/index.spec.ts +1 -3
- package/src/dev/index.ts +9 -3
- package/src/docker/__tests__/templates.spec.ts +3 -1
- package/src/index.ts +88 -0
- package/src/init/__tests__/generators.spec.ts +273 -0
- package/src/init/__tests__/init.spec.ts +3 -3
- package/src/init/generators/auth.ts +1 -0
- package/src/init/generators/config.ts +2 -0
- package/src/init/generators/models.ts +6 -1
- package/src/init/generators/monorepo.ts +3 -0
- package/src/init/generators/ui.ts +1472 -0
- package/src/init/generators/web.ts +134 -87
- package/src/init/index.ts +22 -3
- package/src/init/templates/api.ts +109 -3
- package/src/openapi.ts +99 -13
- package/src/workspace/__tests__/schema.spec.ts +107 -0
- package/src/workspace/schema.ts +314 -4
- package/src/workspace/types.ts +22 -36
- package/dist/dokploy-api-CItuaWTq.mjs +0 -3
- package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
- package/dist/encryption-CQXBZGkt.mjs +0 -3
- package/dist/index-A70abJ1m.d.mts.map +0 -1
- package/dist/index-pOA56MWT.d.cts.map +0 -1
- package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
- package/dist/openapi-D7WwlpPF.cjs.map +0 -1
- package/dist/workspace-CaVW6j2q.cjs.map +0 -1
- package/dist/workspace-DLFRaDc-.mjs.map +0 -1
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import type { DokployStageState } from '../state';
|
|
5
6
|
import {
|
|
6
7
|
createEmptyState,
|
|
7
8
|
getAllAppCredentials,
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
setRedisId,
|
|
25
26
|
writeStageState,
|
|
26
27
|
} from '../state';
|
|
27
|
-
import type { DokployStageState } from '../state';
|
|
28
28
|
|
|
29
29
|
describe('state management', () => {
|
|
30
30
|
let testDir: string;
|
|
@@ -423,7 +423,9 @@ describe('state management', () => {
|
|
|
423
423
|
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
-
expect(
|
|
426
|
+
expect(
|
|
427
|
+
getGeneratedSecret(state, 'api', 'BETTER_AUTH_SECRET'),
|
|
428
|
+
).toBeUndefined();
|
|
427
429
|
});
|
|
428
430
|
|
|
429
431
|
it('should return undefined when secret name not found', () => {
|
|
@@ -445,11 +447,15 @@ describe('state management', () => {
|
|
|
445
447
|
it('should return undefined when no generatedSecrets', () => {
|
|
446
448
|
const state = createEmptyState('production', 'env_123');
|
|
447
449
|
|
|
448
|
-
expect(
|
|
450
|
+
expect(
|
|
451
|
+
getGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET'),
|
|
452
|
+
).toBeUndefined();
|
|
449
453
|
});
|
|
450
454
|
|
|
451
455
|
it('should return undefined when state is null', () => {
|
|
452
|
-
expect(
|
|
456
|
+
expect(
|
|
457
|
+
getGeneratedSecret(null, 'auth', 'BETTER_AUTH_SECRET'),
|
|
458
|
+
).toBeUndefined();
|
|
453
459
|
});
|
|
454
460
|
});
|
|
455
461
|
|
|
@@ -459,7 +465,9 @@ describe('state management', () => {
|
|
|
459
465
|
|
|
460
466
|
setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
|
|
461
467
|
|
|
462
|
-
expect(state.generatedSecrets?.auth?.BETTER_AUTH_SECRET).toBe(
|
|
468
|
+
expect(state.generatedSecrets?.auth?.BETTER_AUTH_SECRET).toBe(
|
|
469
|
+
'secret123',
|
|
470
|
+
);
|
|
463
471
|
});
|
|
464
472
|
|
|
465
473
|
it('should initialize generatedSecrets if not exists', () => {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS Provider Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstracts DNS operations for different providers.
|
|
5
|
+
* Built-in providers: HostingerProvider, Route53Provider
|
|
6
|
+
* Users can also supply custom implementations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { z } from 'zod/v4';
|
|
10
|
+
import type {
|
|
11
|
+
CloudflareDnsProviderSchema,
|
|
12
|
+
CustomDnsProviderSchema,
|
|
13
|
+
DnsProviderSchema,
|
|
14
|
+
DnsRecordSchema,
|
|
15
|
+
DnsRecordTypeSchema,
|
|
16
|
+
HostingerDnsProviderSchema,
|
|
17
|
+
ManualDnsProviderSchema,
|
|
18
|
+
Route53DnsProviderSchema,
|
|
19
|
+
UpsertDnsRecordSchema,
|
|
20
|
+
UpsertResultSchema,
|
|
21
|
+
} from '../../workspace/schema';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// DNS Record Types (derived from Zod schemas)
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DNS record types supported across providers.
|
|
29
|
+
*/
|
|
30
|
+
export type DnsRecordType = z.infer<typeof DnsRecordTypeSchema>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A DNS record as returned by the provider.
|
|
34
|
+
*/
|
|
35
|
+
export type DnsRecord = z.infer<typeof DnsRecordSchema>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A DNS record to create or update.
|
|
39
|
+
*/
|
|
40
|
+
export type UpsertDnsRecord = z.infer<typeof UpsertDnsRecordSchema>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Result of an upsert operation.
|
|
44
|
+
*/
|
|
45
|
+
export type UpsertResult = z.infer<typeof UpsertResultSchema>;
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// DNS Provider Interface
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Interface for DNS providers.
|
|
53
|
+
*
|
|
54
|
+
* Implementations must handle:
|
|
55
|
+
* - Getting all records for a domain
|
|
56
|
+
* - Creating or updating records for a domain
|
|
57
|
+
*/
|
|
58
|
+
export interface DnsProvider {
|
|
59
|
+
/** Provider name for logging */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all DNS records for a domain.
|
|
64
|
+
*
|
|
65
|
+
* @param domain - Root domain (e.g., 'example.com')
|
|
66
|
+
* @returns Array of DNS records
|
|
67
|
+
*/
|
|
68
|
+
getRecords(domain: string): Promise<DnsRecord[]>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create or update DNS records.
|
|
72
|
+
*
|
|
73
|
+
* @param domain - Root domain (e.g., 'example.com')
|
|
74
|
+
* @param records - Records to create or update
|
|
75
|
+
* @returns Results of the upsert operations
|
|
76
|
+
*/
|
|
77
|
+
upsertRecords(
|
|
78
|
+
domain: string,
|
|
79
|
+
records: UpsertDnsRecord[],
|
|
80
|
+
): Promise<UpsertResult[]>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// DNS Provider Config Types (derived from Zod schemas)
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export type HostingerDnsConfig = z.infer<typeof HostingerDnsProviderSchema>;
|
|
88
|
+
export type Route53DnsConfig = z.infer<typeof Route53DnsProviderSchema>;
|
|
89
|
+
export type CloudflareDnsConfig = z.infer<typeof CloudflareDnsProviderSchema>;
|
|
90
|
+
export type ManualDnsConfig = z.infer<typeof ManualDnsProviderSchema>;
|
|
91
|
+
export type CustomDnsConfig = z.infer<typeof CustomDnsProviderSchema>;
|
|
92
|
+
/** Single DNS provider config (for one domain) */
|
|
93
|
+
export type DnsConfig = z.infer<typeof DnsProviderSchema>;
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// DNS Provider Factory
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if value is a DnsProvider implementation.
|
|
101
|
+
*/
|
|
102
|
+
export function isDnsProvider(value: unknown): value is DnsProvider {
|
|
103
|
+
return (
|
|
104
|
+
typeof value === 'object' &&
|
|
105
|
+
value !== null &&
|
|
106
|
+
typeof (value as DnsProvider).name === 'string' &&
|
|
107
|
+
typeof (value as DnsProvider).getRecords === 'function' &&
|
|
108
|
+
typeof (value as DnsProvider).upsertRecords === 'function'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CreateDnsProviderOptions {
|
|
113
|
+
/** DNS config from workspace */
|
|
114
|
+
config: DnsConfig;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a DNS provider based on configuration.
|
|
119
|
+
*
|
|
120
|
+
* - 'hostinger': HostingerProvider
|
|
121
|
+
* - 'route53': Route53Provider
|
|
122
|
+
* - 'manual': Returns null (user handles DNS)
|
|
123
|
+
* - Custom: Use provided DnsProvider implementation
|
|
124
|
+
*/
|
|
125
|
+
export async function createDnsProvider(
|
|
126
|
+
options: CreateDnsProviderOptions,
|
|
127
|
+
): Promise<DnsProvider | null> {
|
|
128
|
+
const { config } = options;
|
|
129
|
+
|
|
130
|
+
// Manual mode - no provider needed
|
|
131
|
+
if (config.provider === 'manual') {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Custom provider implementation
|
|
136
|
+
if (isDnsProvider(config.provider)) {
|
|
137
|
+
return config.provider;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Built-in providers
|
|
141
|
+
const provider = config.provider;
|
|
142
|
+
|
|
143
|
+
if (provider === 'hostinger') {
|
|
144
|
+
const { HostingerProvider } = await import('./HostingerProvider');
|
|
145
|
+
return new HostingerProvider();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (provider === 'route53') {
|
|
149
|
+
const { Route53Provider } = await import('./Route53Provider');
|
|
150
|
+
const route53Config = config as Route53DnsConfig;
|
|
151
|
+
return new Route53Provider({
|
|
152
|
+
region: route53Config.region,
|
|
153
|
+
profile: route53Config.profile,
|
|
154
|
+
hostedZoneId: route53Config.hostedZoneId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (provider === 'cloudflare') {
|
|
159
|
+
throw new Error('Cloudflare DNS provider not yet implemented');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new Error(`Unknown DNS provider: ${JSON.stringify(config)}`);
|
|
163
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hostinger DNS Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements DnsProvider interface using the Hostinger DNS API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getHostingerToken } from '../../auth/credentials';
|
|
8
|
+
import type {
|
|
9
|
+
DnsProvider,
|
|
10
|
+
DnsRecord,
|
|
11
|
+
UpsertDnsRecord,
|
|
12
|
+
UpsertResult,
|
|
13
|
+
} from './DnsProvider';
|
|
14
|
+
import { HostingerApi } from './hostinger-api';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hostinger DNS provider implementation.
|
|
18
|
+
*/
|
|
19
|
+
export class HostingerProvider implements DnsProvider {
|
|
20
|
+
readonly name = 'hostinger';
|
|
21
|
+
private api: HostingerApi | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get or create the Hostinger API client.
|
|
25
|
+
*/
|
|
26
|
+
private async getApi(): Promise<HostingerApi> {
|
|
27
|
+
if (this.api) {
|
|
28
|
+
return this.api;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const token = await getHostingerToken();
|
|
32
|
+
if (!token) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.api = new HostingerApi(token);
|
|
39
|
+
return this.api;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getRecords(domain: string): Promise<DnsRecord[]> {
|
|
43
|
+
const api = await this.getApi();
|
|
44
|
+
const records = await api.getRecords(domain);
|
|
45
|
+
|
|
46
|
+
return records.map((r) => ({
|
|
47
|
+
name: r.name,
|
|
48
|
+
type: r.type,
|
|
49
|
+
ttl: r.ttl,
|
|
50
|
+
values: r.records.map((rec) => rec.content),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async upsertRecords(
|
|
55
|
+
domain: string,
|
|
56
|
+
records: UpsertDnsRecord[],
|
|
57
|
+
): Promise<UpsertResult[]> {
|
|
58
|
+
const api = await this.getApi();
|
|
59
|
+
const results: UpsertResult[] = [];
|
|
60
|
+
|
|
61
|
+
// Get existing records to check what already exists
|
|
62
|
+
const existingRecords = await api.getRecords(domain);
|
|
63
|
+
|
|
64
|
+
for (const record of records) {
|
|
65
|
+
const existing = existingRecords.find(
|
|
66
|
+
(r) => r.name === record.name && r.type === record.type,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const existingValue = existing?.records?.[0]?.content;
|
|
70
|
+
|
|
71
|
+
if (existing && existingValue === record.value) {
|
|
72
|
+
// Record exists with same value - unchanged
|
|
73
|
+
results.push({
|
|
74
|
+
record,
|
|
75
|
+
created: false,
|
|
76
|
+
unchanged: true,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create or update the record
|
|
82
|
+
await api.upsertRecords(domain, [
|
|
83
|
+
{
|
|
84
|
+
name: record.name,
|
|
85
|
+
type: record.type,
|
|
86
|
+
ttl: record.ttl,
|
|
87
|
+
records: [{ content: record.value }],
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
results.push({
|
|
92
|
+
record,
|
|
93
|
+
created: !existing,
|
|
94
|
+
unchanged: false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route53 DNS Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements DnsProvider interface using AWS Route53.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ChangeResourceRecordSetsCommand,
|
|
9
|
+
ListHostedZonesByNameCommand,
|
|
10
|
+
ListResourceRecordSetsCommand,
|
|
11
|
+
Route53Client,
|
|
12
|
+
type RRType,
|
|
13
|
+
} from '@aws-sdk/client-route-53';
|
|
14
|
+
import { fromIni } from '@aws-sdk/credential-providers';
|
|
15
|
+
import type {
|
|
16
|
+
DnsProvider,
|
|
17
|
+
DnsRecord,
|
|
18
|
+
DnsRecordType,
|
|
19
|
+
UpsertDnsRecord,
|
|
20
|
+
UpsertResult,
|
|
21
|
+
} from './DnsProvider';
|
|
22
|
+
|
|
23
|
+
export interface Route53ProviderOptions {
|
|
24
|
+
/** AWS region (optional - uses AWS_REGION env var if not provided) */
|
|
25
|
+
region?: string;
|
|
26
|
+
/** AWS profile name (optional - uses default credential chain if not provided) */
|
|
27
|
+
profile?: string;
|
|
28
|
+
/** Hosted zone ID (optional - auto-detected from domain if not provided) */
|
|
29
|
+
hostedZoneId?: string;
|
|
30
|
+
/** Custom endpoint for testing with localstack */
|
|
31
|
+
endpoint?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Route53 DNS provider implementation.
|
|
36
|
+
*
|
|
37
|
+
* Uses AWS default credential chain for authentication.
|
|
38
|
+
* Region can be specified or will use AWS_REGION/AWS_DEFAULT_REGION env vars.
|
|
39
|
+
* Profile can be specified to use a named profile from ~/.aws/credentials.
|
|
40
|
+
*/
|
|
41
|
+
export class Route53Provider implements DnsProvider {
|
|
42
|
+
readonly name = 'route53';
|
|
43
|
+
private client: Route53Client;
|
|
44
|
+
private hostedZoneId?: string;
|
|
45
|
+
private hostedZoneCache: Map<string, string> = new Map();
|
|
46
|
+
|
|
47
|
+
constructor(options: Route53ProviderOptions = {}) {
|
|
48
|
+
this.client = new Route53Client({
|
|
49
|
+
...(options.region && { region: options.region }),
|
|
50
|
+
...(options.endpoint && { endpoint: options.endpoint }),
|
|
51
|
+
...(options.profile && {
|
|
52
|
+
credentials: fromIni({ profile: options.profile }),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
this.hostedZoneId = options.hostedZoneId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the hosted zone ID for a domain.
|
|
60
|
+
* Uses cache to avoid repeated API calls.
|
|
61
|
+
*/
|
|
62
|
+
private async getHostedZoneId(domain: string): Promise<string> {
|
|
63
|
+
// Use configured zone ID if provided
|
|
64
|
+
if (this.hostedZoneId) {
|
|
65
|
+
return this.hostedZoneId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check cache
|
|
69
|
+
if (this.hostedZoneCache.has(domain)) {
|
|
70
|
+
return this.hostedZoneCache.get(domain)!;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Auto-detect from domain
|
|
74
|
+
const command = new ListHostedZonesByNameCommand({
|
|
75
|
+
DNSName: domain,
|
|
76
|
+
MaxItems: 1,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const response = await this.client.send(command);
|
|
80
|
+
const zones = response.HostedZones ?? [];
|
|
81
|
+
|
|
82
|
+
// Find exact match (domain with trailing dot)
|
|
83
|
+
const normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;
|
|
84
|
+
const zone = zones.find((z) => z.Name === normalizedDomain);
|
|
85
|
+
|
|
86
|
+
if (!zone?.Id) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`No hosted zone found for domain: ${domain}. Create one in Route53 or provide hostedZoneId in config.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Zone ID comes as "/hostedzone/Z1234567890" - extract just the ID
|
|
93
|
+
const zoneId = zone.Id.replace('/hostedzone/', '');
|
|
94
|
+
this.hostedZoneCache.set(domain, zoneId);
|
|
95
|
+
return zoneId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert Route53 record type to our DnsRecordType.
|
|
100
|
+
* Excludes NS and SOA which are auto-managed by Route53 for the zone.
|
|
101
|
+
*/
|
|
102
|
+
private toRecordType(type: string): DnsRecordType | null {
|
|
103
|
+
// Exclude NS and SOA which are auto-managed zone records
|
|
104
|
+
const managedTypes = ['NS', 'SOA'];
|
|
105
|
+
if (managedTypes.includes(type)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const validTypes: DnsRecordType[] = [
|
|
110
|
+
'A',
|
|
111
|
+
'AAAA',
|
|
112
|
+
'CNAME',
|
|
113
|
+
'MX',
|
|
114
|
+
'TXT',
|
|
115
|
+
'SRV',
|
|
116
|
+
'CAA',
|
|
117
|
+
];
|
|
118
|
+
return validTypes.includes(type as DnsRecordType)
|
|
119
|
+
? (type as DnsRecordType)
|
|
120
|
+
: null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract subdomain from full record name relative to domain.
|
|
125
|
+
*/
|
|
126
|
+
private extractSubdomain(recordName: string, domain: string): string {
|
|
127
|
+
const normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;
|
|
128
|
+
const normalizedName = recordName.endsWith('.')
|
|
129
|
+
? recordName
|
|
130
|
+
: `${recordName}.`;
|
|
131
|
+
|
|
132
|
+
if (normalizedName === normalizedDomain) {
|
|
133
|
+
return '@';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove the domain suffix
|
|
137
|
+
const subdomain = normalizedName.replace(`.${normalizedDomain}`, '');
|
|
138
|
+
return subdomain.replace(/\.$/, ''); // Remove trailing dot if any
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getRecords(domain: string): Promise<DnsRecord[]> {
|
|
142
|
+
const zoneId = await this.getHostedZoneId(domain);
|
|
143
|
+
const records: DnsRecord[] = [];
|
|
144
|
+
|
|
145
|
+
let nextRecordName: string | undefined;
|
|
146
|
+
let nextRecordType: RRType | undefined;
|
|
147
|
+
|
|
148
|
+
// Paginate through all records
|
|
149
|
+
do {
|
|
150
|
+
const command = new ListResourceRecordSetsCommand({
|
|
151
|
+
HostedZoneId: zoneId,
|
|
152
|
+
StartRecordName: nextRecordName,
|
|
153
|
+
StartRecordType: nextRecordType,
|
|
154
|
+
MaxItems: 100,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const response = await this.client.send(command);
|
|
158
|
+
|
|
159
|
+
for (const recordSet of response.ResourceRecordSets ?? []) {
|
|
160
|
+
const type = this.toRecordType(recordSet.Type ?? '');
|
|
161
|
+
if (!type || !recordSet.Name) continue;
|
|
162
|
+
|
|
163
|
+
const values = (recordSet.ResourceRecords ?? [])
|
|
164
|
+
.map((r) => r.Value)
|
|
165
|
+
.filter((v): v is string => !!v);
|
|
166
|
+
|
|
167
|
+
records.push({
|
|
168
|
+
name: this.extractSubdomain(recordSet.Name, domain),
|
|
169
|
+
type,
|
|
170
|
+
ttl: recordSet.TTL ?? 300,
|
|
171
|
+
values,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (response.IsTruncated) {
|
|
176
|
+
nextRecordName = response.NextRecordName;
|
|
177
|
+
nextRecordType = response.NextRecordType;
|
|
178
|
+
} else {
|
|
179
|
+
nextRecordName = undefined;
|
|
180
|
+
}
|
|
181
|
+
} while (nextRecordName);
|
|
182
|
+
|
|
183
|
+
return records;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async upsertRecords(
|
|
187
|
+
domain: string,
|
|
188
|
+
records: UpsertDnsRecord[],
|
|
189
|
+
): Promise<UpsertResult[]> {
|
|
190
|
+
const zoneId = await this.getHostedZoneId(domain);
|
|
191
|
+
const results: UpsertResult[] = [];
|
|
192
|
+
|
|
193
|
+
// Get existing records to determine if creating or updating
|
|
194
|
+
const existingRecords = await this.getRecords(domain);
|
|
195
|
+
|
|
196
|
+
// Process records in batches (Route53 allows max 1000 changes per request)
|
|
197
|
+
const batchSize = 100;
|
|
198
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
199
|
+
const batch = records.slice(i, i + batchSize);
|
|
200
|
+
const changes = [];
|
|
201
|
+
|
|
202
|
+
for (const record of batch) {
|
|
203
|
+
const existing = existingRecords.find(
|
|
204
|
+
(r) => r.name === record.name && r.type === record.type,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const existingValue = existing?.values?.[0];
|
|
208
|
+
|
|
209
|
+
if (existing && existingValue === record.value) {
|
|
210
|
+
// Record exists with same value - unchanged
|
|
211
|
+
results.push({
|
|
212
|
+
record,
|
|
213
|
+
created: false,
|
|
214
|
+
unchanged: true,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build full record name
|
|
220
|
+
const recordName =
|
|
221
|
+
record.name === '@' ? domain : `${record.name}.${domain}`;
|
|
222
|
+
|
|
223
|
+
changes.push({
|
|
224
|
+
Action: 'UPSERT' as const,
|
|
225
|
+
ResourceRecordSet: {
|
|
226
|
+
Name: recordName,
|
|
227
|
+
Type: record.type,
|
|
228
|
+
TTL: record.ttl,
|
|
229
|
+
ResourceRecords: [{ Value: record.value }],
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
results.push({
|
|
234
|
+
record,
|
|
235
|
+
created: !existing,
|
|
236
|
+
unchanged: false,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Execute batch if there are changes
|
|
241
|
+
if (changes.length > 0) {
|
|
242
|
+
const command = new ChangeResourceRecordSetsCommand({
|
|
243
|
+
HostedZoneId: zoneId,
|
|
244
|
+
ChangeBatch: {
|
|
245
|
+
Comment: 'Upsert by gkm deploy',
|
|
246
|
+
Changes: changes,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await this.client.send(command);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
256
|
+
}
|