@rulebricks/cli 1.9.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 (93) hide show
  1. package/README.md +62 -0
  2. package/dist/commands/clone.d.ts +6 -0
  3. package/dist/commands/clone.js +60 -0
  4. package/dist/commands/deploy.d.ts +8 -0
  5. package/dist/commands/deploy.js +409 -0
  6. package/dist/commands/destroy.d.ts +8 -0
  7. package/dist/commands/destroy.js +298 -0
  8. package/dist/commands/init.d.ts +7 -0
  9. package/dist/commands/init.js +201 -0
  10. package/dist/commands/logs.d.ts +9 -0
  11. package/dist/commands/logs.js +222 -0
  12. package/dist/commands/open.d.ts +7 -0
  13. package/dist/commands/open.js +139 -0
  14. package/dist/commands/status.d.ts +5 -0
  15. package/dist/commands/status.js +125 -0
  16. package/dist/commands/upgrade.d.ts +7 -0
  17. package/dist/commands/upgrade.js +239 -0
  18. package/dist/components/DNSWaitScreen.d.ts +9 -0
  19. package/dist/components/DNSWaitScreen.js +73 -0
  20. package/dist/components/Wizard/WizardContext.d.ts +176 -0
  21. package/dist/components/Wizard/WizardContext.js +346 -0
  22. package/dist/components/Wizard/index.d.ts +2 -0
  23. package/dist/components/Wizard/index.js +2 -0
  24. package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
  25. package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
  26. package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
  27. package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
  28. package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
  29. package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
  30. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
  31. package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
  32. package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
  33. package/dist/components/Wizard/steps/DomainStep.js +126 -0
  34. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
  35. package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
  36. package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
  38. package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
  39. package/dist/components/Wizard/steps/ReviewStep.js +56 -0
  40. package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
  41. package/dist/components/Wizard/steps/SMTPStep.js +191 -0
  42. package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
  43. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
  44. package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
  45. package/dist/components/Wizard/steps/TierStep.js +29 -0
  46. package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
  47. package/dist/components/Wizard/steps/VersionStep.js +113 -0
  48. package/dist/components/Wizard/steps/index.d.ts +12 -0
  49. package/dist/components/Wizard/steps/index.js +12 -0
  50. package/dist/components/common/AppShell.d.ts +31 -0
  51. package/dist/components/common/AppShell.js +31 -0
  52. package/dist/components/common/Box.d.ts +20 -0
  53. package/dist/components/common/Box.js +20 -0
  54. package/dist/components/common/Logo.d.ts +7 -0
  55. package/dist/components/common/Logo.js +22 -0
  56. package/dist/components/common/Spinner.d.ts +12 -0
  57. package/dist/components/common/Spinner.js +28 -0
  58. package/dist/components/common/index.d.ts +6 -0
  59. package/dist/components/common/index.js +5 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +202 -0
  62. package/dist/lib/cloudCli.d.ts +156 -0
  63. package/dist/lib/cloudCli.js +691 -0
  64. package/dist/lib/config.d.ts +91 -0
  65. package/dist/lib/config.js +278 -0
  66. package/dist/lib/dns.d.ts +41 -0
  67. package/dist/lib/dns.js +235 -0
  68. package/dist/lib/dockerHub.d.ts +57 -0
  69. package/dist/lib/dockerHub.js +128 -0
  70. package/dist/lib/helm.d.ts +53 -0
  71. package/dist/lib/helm.js +209 -0
  72. package/dist/lib/helmValues.d.ts +17 -0
  73. package/dist/lib/helmValues.js +693 -0
  74. package/dist/lib/kubernetes.d.ts +161 -0
  75. package/dist/lib/kubernetes.js +755 -0
  76. package/dist/lib/terraform.d.ts +44 -0
  77. package/dist/lib/terraform.js +230 -0
  78. package/dist/lib/theme.d.ts +81 -0
  79. package/dist/lib/theme.js +115 -0
  80. package/dist/lib/validation.d.ts +47 -0
  81. package/dist/lib/validation.js +164 -0
  82. package/dist/lib/versions.d.ts +69 -0
  83. package/dist/lib/versions.js +139 -0
  84. package/dist/types/index.d.ts +718 -0
  85. package/dist/types/index.js +556 -0
  86. package/email-templates/email_change.html +325 -0
  87. package/email-templates/invite.html +383 -0
  88. package/email-templates/password_change.html +414 -0
  89. package/email-templates/verify.html +396 -0
  90. package/package.json +78 -0
  91. package/terraform/aws/main.tf +327 -0
  92. package/terraform/azure/main.tf +326 -0
  93. package/terraform/gcp/main.tf +369 -0
@@ -0,0 +1,44 @@
1
+ import { CloudProvider } from '../types/index.js';
2
+ /**
3
+ * Checks if Terraform is installed
4
+ */
5
+ export declare function isTerraformInstalled(): Promise<boolean>;
6
+ /**
7
+ * Gets the installed Terraform version
8
+ */
9
+ export declare function getTerraformVersion(): Promise<string>;
10
+ /**
11
+ * Copies terraform templates to the deployment directory
12
+ */
13
+ export declare function setupTerraformWorkspace(deploymentName: string, provider: CloudProvider): Promise<string>;
14
+ /**
15
+ * Initializes Terraform in the deployment directory
16
+ */
17
+ export declare function terraformInit(deploymentName: string): Promise<void>;
18
+ /**
19
+ * Plans Terraform changes
20
+ */
21
+ export declare function terraformPlan(deploymentName: string): Promise<void>;
22
+ /**
23
+ * Applies Terraform changes
24
+ */
25
+ export declare function terraformApply(deploymentName: string): Promise<void>;
26
+ /**
27
+ * Destroys Terraform infrastructure
28
+ */
29
+ export declare function terraformDestroy(deploymentName: string): Promise<void>;
30
+ /**
31
+ * Gets Terraform outputs
32
+ */
33
+ export declare function getTerraformOutputs(deploymentName: string): Promise<Record<string, string>>;
34
+ /**
35
+ * Checks if Terraform state exists for a deployment
36
+ */
37
+ export declare function hasTerraformState(deploymentName: string): Promise<boolean>;
38
+ /**
39
+ * Updates kubeconfig for the provisioned cluster
40
+ */
41
+ export declare function updateKubeconfig(provider: CloudProvider, clusterName: string, region: string, options?: {
42
+ gcpProjectId?: string;
43
+ azureResourceGroup?: string;
44
+ }): Promise<void>;
@@ -0,0 +1,230 @@
1
+ import { execa } from 'execa';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { getTerraformDir } from './config.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ // Path to embedded terraform templates
9
+ const TERRAFORM_TEMPLATES_DIR = path.resolve(__dirname, '../../terraform');
10
+ /**
11
+ * Extracts meaningful error message from execa error
12
+ */
13
+ function getErrorMessage(error, fallback) {
14
+ const execaError = error;
15
+ // Try stderr first, then stdout (terraform sometimes writes errors to stdout)
16
+ const output = execaError.stderr || execaError.stdout || '';
17
+ if (output) {
18
+ // Get last 500 chars of output for the error message
19
+ const truncated = output.length > 500 ? '...' + output.slice(-500) : output;
20
+ return truncated;
21
+ }
22
+ return execaError.shortMessage || execaError.message || fallback;
23
+ }
24
+ /**
25
+ * Saves command output to a log file
26
+ */
27
+ async function saveLogFile(workDir, command, stdout, stderr) {
28
+ const logFile = path.join(workDir, `${command}-${Date.now()}.log`);
29
+ const content = `=== STDOUT ===\n${stdout}\n\n=== STDERR ===\n${stderr}`;
30
+ await fs.writeFile(logFile, content);
31
+ return logFile;
32
+ }
33
+ /**
34
+ * Checks if Terraform is installed
35
+ */
36
+ export async function isTerraformInstalled() {
37
+ try {
38
+ await execa('terraform', ['version']);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Gets the installed Terraform version
47
+ */
48
+ export async function getTerraformVersion() {
49
+ const { stdout } = await execa('terraform', ['version', '-json']);
50
+ const info = JSON.parse(stdout);
51
+ return info.terraform_version;
52
+ }
53
+ /**
54
+ * Copies terraform templates to the deployment directory
55
+ */
56
+ export async function setupTerraformWorkspace(deploymentName, provider) {
57
+ const sourceDir = path.join(TERRAFORM_TEMPLATES_DIR, provider);
58
+ const targetDir = getTerraformDir(deploymentName);
59
+ // Create target directory
60
+ await fs.mkdir(targetDir, { recursive: true });
61
+ // Copy all terraform files
62
+ await copyDirectory(sourceDir, targetDir);
63
+ return targetDir;
64
+ }
65
+ /**
66
+ * Recursively copies a directory
67
+ */
68
+ async function copyDirectory(src, dest) {
69
+ await fs.mkdir(dest, { recursive: true });
70
+ const entries = await fs.readdir(src, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ const srcPath = path.join(src, entry.name);
73
+ const destPath = path.join(dest, entry.name);
74
+ if (entry.isDirectory()) {
75
+ await copyDirectory(srcPath, destPath);
76
+ }
77
+ else {
78
+ await fs.copyFile(srcPath, destPath);
79
+ }
80
+ }
81
+ }
82
+ /**
83
+ * Initializes Terraform in the deployment directory
84
+ */
85
+ export async function terraformInit(deploymentName) {
86
+ const workDir = getTerraformDir(deploymentName);
87
+ try {
88
+ // Use 'pipe' to capture output instead of 'inherit' to avoid
89
+ // interfering with Ink's terminal rendering
90
+ await execa('terraform', ['init', '-upgrade'], {
91
+ cwd: workDir
92
+ });
93
+ }
94
+ catch (error) {
95
+ const execaError = error;
96
+ // Save logs for debugging
97
+ if (execaError.stdout || execaError.stderr) {
98
+ await saveLogFile(workDir, 'init', execaError.stdout || '', execaError.stderr || '');
99
+ }
100
+ throw new Error(`Terraform init failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
101
+ }
102
+ }
103
+ /**
104
+ * Plans Terraform changes
105
+ */
106
+ export async function terraformPlan(deploymentName) {
107
+ const workDir = getTerraformDir(deploymentName);
108
+ try {
109
+ await execa('terraform', ['plan', '-out=tfplan'], {
110
+ cwd: workDir
111
+ });
112
+ }
113
+ catch (error) {
114
+ const execaError = error;
115
+ if (execaError.stdout || execaError.stderr) {
116
+ await saveLogFile(workDir, 'plan', execaError.stdout || '', execaError.stderr || '');
117
+ }
118
+ throw new Error(`Terraform plan failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
119
+ }
120
+ }
121
+ /**
122
+ * Applies Terraform changes
123
+ */
124
+ export async function terraformApply(deploymentName) {
125
+ const workDir = getTerraformDir(deploymentName);
126
+ try {
127
+ await execa('terraform', ['apply', '-auto-approve', 'tfplan'], {
128
+ cwd: workDir
129
+ });
130
+ }
131
+ catch (error) {
132
+ const execaError = error;
133
+ if (execaError.stdout || execaError.stderr) {
134
+ await saveLogFile(workDir, 'apply', execaError.stdout || '', execaError.stderr || '');
135
+ }
136
+ throw new Error(`Terraform apply failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
137
+ }
138
+ }
139
+ /**
140
+ * Destroys Terraform infrastructure
141
+ */
142
+ export async function terraformDestroy(deploymentName) {
143
+ const workDir = getTerraformDir(deploymentName);
144
+ try {
145
+ await execa('terraform', ['destroy', '-auto-approve'], {
146
+ cwd: workDir
147
+ });
148
+ }
149
+ catch (error) {
150
+ const execaError = error;
151
+ if (execaError.stdout || execaError.stderr) {
152
+ await saveLogFile(workDir, 'destroy', execaError.stdout || '', execaError.stderr || '');
153
+ }
154
+ throw new Error(`Terraform destroy failed:\n${getErrorMessage(error, 'Unknown error')}\n\nLogs saved to: ${workDir}`);
155
+ }
156
+ }
157
+ /**
158
+ * Gets Terraform outputs
159
+ */
160
+ export async function getTerraformOutputs(deploymentName) {
161
+ const workDir = getTerraformDir(deploymentName);
162
+ try {
163
+ const { stdout } = await execa('terraform', ['output', '-json'], {
164
+ cwd: workDir
165
+ });
166
+ const outputs = JSON.parse(stdout);
167
+ const result = {};
168
+ for (const [key, data] of Object.entries(outputs)) {
169
+ result[key] = String(data.value);
170
+ }
171
+ return result;
172
+ }
173
+ catch {
174
+ return {};
175
+ }
176
+ }
177
+ /**
178
+ * Checks if Terraform state exists for a deployment
179
+ */
180
+ export async function hasTerraformState(deploymentName) {
181
+ const workDir = getTerraformDir(deploymentName);
182
+ const statePath = path.join(workDir, 'terraform.tfstate');
183
+ try {
184
+ await fs.access(statePath);
185
+ return true;
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Updates kubeconfig for the provisioned cluster
193
+ */
194
+ export async function updateKubeconfig(provider, clusterName, region, options = {}) {
195
+ try {
196
+ switch (provider) {
197
+ case 'aws':
198
+ await execa('aws', [
199
+ 'eks', 'update-kubeconfig',
200
+ '--name', clusterName,
201
+ '--region', region
202
+ ]);
203
+ break;
204
+ case 'gcp':
205
+ if (!options.gcpProjectId) {
206
+ throw new Error('GCP project ID is required');
207
+ }
208
+ await execa('gcloud', [
209
+ 'container', 'clusters', 'get-credentials',
210
+ clusterName,
211
+ '--region', region,
212
+ '--project', options.gcpProjectId
213
+ ]);
214
+ break;
215
+ case 'azure':
216
+ if (!options.azureResourceGroup) {
217
+ throw new Error('Azure resource group is required');
218
+ }
219
+ await execa('az', [
220
+ 'aks', 'get-credentials',
221
+ '--name', clusterName,
222
+ '--resource-group', options.azureResourceGroup
223
+ ]);
224
+ break;
225
+ }
226
+ }
227
+ catch (error) {
228
+ throw new Error(`Failed to update kubeconfig:\n${getErrorMessage(error, 'Unknown error')}`);
229
+ }
230
+ }
@@ -0,0 +1,81 @@
1
+ import React, { ReactNode } from "react";
2
+ /**
3
+ * Command theme types - each command has its own visual identity
4
+ */
5
+ export type CommandTheme = "init" | "deploy" | "upgrade" | "destroy" | "status" | "logs";
6
+ /**
7
+ * Theme color configuration
8
+ */
9
+ export interface ThemeColors {
10
+ /** Primary accent color for borders, highlights */
11
+ accent: string;
12
+ /** Brighter variant for emphasis */
13
+ accentBright: string;
14
+ /** Color for selected/active items */
15
+ selected: string;
16
+ /** Color for success states */
17
+ success: string;
18
+ /** Color for error states */
19
+ error: string;
20
+ /** Color for warning states */
21
+ warning: string;
22
+ /** Dimmed/muted color */
23
+ muted: string;
24
+ }
25
+ /**
26
+ * Theme definitions for each command
27
+ *
28
+ * - init: Magenta - fresh start, creative setup
29
+ * - deploy: Blue - action, progress, trust
30
+ * - upgrade: #ea9d34 - caution, change, attention
31
+ * - destroy: Red - danger, destructive action
32
+ * - status: #4c9c81 - health, success, information
33
+ * - logs: #c2b5ab - neutral, observational
34
+ */
35
+ export declare const THEMES: Record<CommandTheme, ThemeColors>;
36
+ /**
37
+ * Default theme (used when no provider is present)
38
+ */
39
+ export declare const DEFAULT_THEME: CommandTheme;
40
+ /**
41
+ * Theme context value
42
+ */
43
+ interface ThemeContextValue {
44
+ theme: CommandTheme;
45
+ colors: ThemeColors;
46
+ }
47
+ /**
48
+ * ThemeProvider props
49
+ */
50
+ interface ThemeProviderProps {
51
+ theme: CommandTheme;
52
+ children: ReactNode;
53
+ }
54
+ /**
55
+ * ThemeProvider component - wraps a command to provide themed styling
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * <ThemeProvider theme="destroy">
60
+ * <DestroyCommand />
61
+ * </ThemeProvider>
62
+ * ```
63
+ */
64
+ export declare function ThemeProvider({ theme, children, }: ThemeProviderProps): React.ReactElement;
65
+ /**
66
+ * Hook to access current theme colors
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * function MyComponent() {
71
+ * const { colors } = useTheme();
72
+ * return <Text color={colors.accent}>Themed text</Text>;
73
+ * }
74
+ * ```
75
+ */
76
+ export declare function useTheme(): ThemeContextValue;
77
+ /**
78
+ * Get theme colors directly without hook (for non-component code)
79
+ */
80
+ export declare function getThemeColors(theme: CommandTheme): ThemeColors;
81
+ export {};
@@ -0,0 +1,115 @@
1
+ import React, { createContext, useContext } from "react";
2
+ /**
3
+ * Theme definitions for each command
4
+ *
5
+ * - init: Magenta - fresh start, creative setup
6
+ * - deploy: Blue - action, progress, trust
7
+ * - upgrade: #ea9d34 - caution, change, attention
8
+ * - destroy: Red - danger, destructive action
9
+ * - status: #4c9c81 - health, success, information
10
+ * - logs: #c2b5ab - neutral, observational
11
+ */
12
+ export const THEMES = {
13
+ init: {
14
+ accent: "#c4a7e7",
15
+ accentBright: "#a78bc7",
16
+ selected: "#c4a7e7",
17
+ success: "#4c9c81",
18
+ error: "#d7827e",
19
+ warning: "#ea9d34",
20
+ muted: "#c2b5ab",
21
+ },
22
+ deploy: {
23
+ accent: "#64a5b0",
24
+ accentBright: "#5fbac9",
25
+ selected: "#64a5b0",
26
+ success: "#4c9c81",
27
+ error: "#d7827e",
28
+ warning: "#ea9d34",
29
+ muted: "#c2b5ab",
30
+ },
31
+ upgrade: {
32
+ accent: "#3e8fb0",
33
+ accentBright: "#5aabcc",
34
+ selected: "#3e8fb0",
35
+ success: "#4c9c81",
36
+ error: "#d7827e",
37
+ warning: "#ea9d34",
38
+ muted: "#c2b5ab",
39
+ },
40
+ destroy: {
41
+ accent: "#d7827e",
42
+ accentBright: "#ea9a97",
43
+ selected: "#d7827e",
44
+ success: "#4c9c81",
45
+ error: "#d7827e",
46
+ warning: "#cf6d69",
47
+ muted: "#a18581",
48
+ },
49
+ status: {
50
+ accent: "#4c9c81",
51
+ accentBright: "#4c9c81",
52
+ selected: "#4c9c81",
53
+ success: "#4c9c81",
54
+ error: "#d7827e",
55
+ warning: "#ea9d34",
56
+ muted: "#c2b5ab",
57
+ },
58
+ logs: {
59
+ accent: "#524f67",
60
+ accentBright: "#56526e",
61
+ selected: "#cecacd",
62
+ success: "#4c9c81",
63
+ error: "#d7827e",
64
+ warning: "#ea9d34",
65
+ muted: "#c2b5ab",
66
+ },
67
+ };
68
+ /**
69
+ * Default theme (used when no provider is present)
70
+ */
71
+ export const DEFAULT_THEME = "init";
72
+ /**
73
+ * Theme context - provides current theme to all child components
74
+ */
75
+ const ThemeContext = createContext({
76
+ theme: DEFAULT_THEME,
77
+ colors: THEMES[DEFAULT_THEME],
78
+ });
79
+ /**
80
+ * ThemeProvider component - wraps a command to provide themed styling
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * <ThemeProvider theme="destroy">
85
+ * <DestroyCommand />
86
+ * </ThemeProvider>
87
+ * ```
88
+ */
89
+ export function ThemeProvider({ theme, children, }) {
90
+ const value = {
91
+ theme,
92
+ colors: THEMES[theme],
93
+ };
94
+ return React.createElement(ThemeContext.Provider, { value }, children);
95
+ }
96
+ /**
97
+ * Hook to access current theme colors
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * function MyComponent() {
102
+ * const { colors } = useTheme();
103
+ * return <Text color={colors.accent}>Themed text</Text>;
104
+ * }
105
+ * ```
106
+ */
107
+ export function useTheme() {
108
+ return useContext(ThemeContext);
109
+ }
110
+ /**
111
+ * Get theme colors directly without hook (for non-component code)
112
+ */
113
+ export function getThemeColors(theme) {
114
+ return THEMES[theme];
115
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Validates an email address using a comprehensive regex
3
+ */
4
+ export declare function isValidEmail(email: string): boolean;
5
+ /**
6
+ * Extracts the base domain from a full domain name
7
+ * e.g., "rulebricks.example.com" -> "example.com"
8
+ */
9
+ export declare function extractBaseDomain(fullDomain: string): string;
10
+ /**
11
+ * Validates that a domain is properly formatted
12
+ */
13
+ export declare function isValidDomainFormat(domain: string): boolean;
14
+ /**
15
+ * Checks if the base domain has active DNS records
16
+ */
17
+ export declare function validateBaseDomain(fullDomain: string): Promise<{
18
+ valid: boolean;
19
+ baseDomain: string;
20
+ error?: string;
21
+ }>;
22
+ /**
23
+ * Checks if a specific hostname resolves to a given target
24
+ */
25
+ export declare function checkDNSRecord(hostname: string, expectedTarget?: string): Promise<{
26
+ resolved: boolean;
27
+ records: string[];
28
+ matchesTarget: boolean;
29
+ }>;
30
+ /**
31
+ * Validates SMTP configuration format
32
+ */
33
+ export declare function validateSMTPConfig(config: {
34
+ host: string;
35
+ port: number;
36
+ user: string;
37
+ pass: string;
38
+ from: string;
39
+ fromName: string;
40
+ }): {
41
+ valid: boolean;
42
+ errors: string[];
43
+ };
44
+ /**
45
+ * Generates a secure random string for secrets
46
+ */
47
+ export declare function generateSecureSecret(length?: number): string;
@@ -0,0 +1,164 @@
1
+ import dns from 'dns';
2
+ import { promisify } from 'util';
3
+ const resolveDns = promisify(dns.resolve);
4
+ const resolve4 = promisify(dns.resolve4);
5
+ const resolveCname = promisify(dns.resolveCname);
6
+ /**
7
+ * Validates an email address using a comprehensive regex
8
+ */
9
+ export function isValidEmail(email) {
10
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
11
+ return emailRegex.test(email) && email.length <= 254;
12
+ }
13
+ /**
14
+ * Extracts the base domain from a full domain name
15
+ * e.g., "rulebricks.example.com" -> "example.com"
16
+ */
17
+ export function extractBaseDomain(fullDomain) {
18
+ const parts = fullDomain.toLowerCase().split('.');
19
+ // Handle common multi-part TLDs
20
+ const multiPartTlds = ['co.uk', 'com.au', 'co.nz', 'co.jp', 'com.br', 'co.za'];
21
+ if (parts.length >= 3) {
22
+ const lastTwo = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
23
+ if (multiPartTlds.includes(lastTwo)) {
24
+ // For multi-part TLDs, take the last 3 parts
25
+ return parts.slice(-3).join('.');
26
+ }
27
+ }
28
+ // For standard TLDs, take the last 2 parts
29
+ if (parts.length >= 2) {
30
+ return parts.slice(-2).join('.');
31
+ }
32
+ return fullDomain;
33
+ }
34
+ /**
35
+ * Validates that a domain is properly formatted
36
+ */
37
+ export function isValidDomainFormat(domain) {
38
+ // Basic domain format validation
39
+ const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
40
+ return domainRegex.test(domain);
41
+ }
42
+ /**
43
+ * Checks if the base domain has active DNS records
44
+ */
45
+ export async function validateBaseDomain(fullDomain) {
46
+ if (!isValidDomainFormat(fullDomain)) {
47
+ return {
48
+ valid: false,
49
+ baseDomain: '',
50
+ error: 'Invalid domain format'
51
+ };
52
+ }
53
+ const baseDomain = extractBaseDomain(fullDomain);
54
+ try {
55
+ // Try to resolve the base domain
56
+ // First try A records
57
+ try {
58
+ await resolve4(baseDomain);
59
+ return { valid: true, baseDomain };
60
+ }
61
+ catch {
62
+ // If A record fails, try any record type
63
+ await resolveDns(baseDomain, 'ANY');
64
+ return { valid: true, baseDomain };
65
+ }
66
+ }
67
+ catch (err) {
68
+ const error = err;
69
+ if (error.code === 'ENOTFOUND') {
70
+ return {
71
+ valid: false,
72
+ baseDomain,
73
+ error: `Base domain "${baseDomain}" does not exist or has no DNS records`
74
+ };
75
+ }
76
+ if (error.code === 'ENODATA') {
77
+ // Domain exists but no records of the requested type
78
+ // This is actually OK for our purposes
79
+ return { valid: true, baseDomain };
80
+ }
81
+ // For other errors, assume the domain might be valid
82
+ // (could be temporary DNS issues)
83
+ return { valid: true, baseDomain };
84
+ }
85
+ }
86
+ /**
87
+ * Checks if a specific hostname resolves to a given target
88
+ */
89
+ export async function checkDNSRecord(hostname, expectedTarget) {
90
+ try {
91
+ // Try A record first
92
+ try {
93
+ const aRecords = await resolve4(hostname);
94
+ const matchesTarget = expectedTarget
95
+ ? aRecords.some(r => r === expectedTarget)
96
+ : true;
97
+ return {
98
+ resolved: true,
99
+ records: aRecords,
100
+ matchesTarget
101
+ };
102
+ }
103
+ catch {
104
+ // Try CNAME
105
+ const cnameRecords = await resolveCname(hostname);
106
+ const matchesTarget = expectedTarget
107
+ ? cnameRecords.some(r => r === expectedTarget || r.endsWith(expectedTarget))
108
+ : true;
109
+ return {
110
+ resolved: true,
111
+ records: cnameRecords,
112
+ matchesTarget
113
+ };
114
+ }
115
+ }
116
+ catch {
117
+ return {
118
+ resolved: false,
119
+ records: [],
120
+ matchesTarget: false
121
+ };
122
+ }
123
+ }
124
+ /**
125
+ * Validates SMTP configuration format
126
+ */
127
+ export function validateSMTPConfig(config) {
128
+ const errors = [];
129
+ if (!config.host || config.host.length < 3) {
130
+ errors.push('SMTP host is required');
131
+ }
132
+ if (!config.port || config.port < 1 || config.port > 65535) {
133
+ errors.push('SMTP port must be between 1 and 65535');
134
+ }
135
+ if (!config.user) {
136
+ errors.push('SMTP username is required');
137
+ }
138
+ if (!config.pass) {
139
+ errors.push('SMTP password is required');
140
+ }
141
+ if (!config.from || !isValidEmail(config.from)) {
142
+ errors.push('Valid SMTP from address is required');
143
+ }
144
+ if (!config.fromName || config.fromName.length < 1) {
145
+ errors.push('SMTP from name is required');
146
+ }
147
+ return {
148
+ valid: errors.length === 0,
149
+ errors
150
+ };
151
+ }
152
+ /**
153
+ * Generates a secure random string for secrets
154
+ */
155
+ export function generateSecureSecret(length = 32) {
156
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
157
+ let result = '';
158
+ const randomValues = new Uint8Array(length);
159
+ crypto.getRandomValues(randomValues);
160
+ for (let i = 0; i < length; i++) {
161
+ result += chars[randomValues[i] % chars.length];
162
+ }
163
+ return result;
164
+ }