@stacksjs/rpx 0.11.7 → 0.11.10

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/src/https.ts CHANGED
@@ -9,8 +9,39 @@ import * as process from 'node:process'
9
9
  import { addCertToSystemTrustStoreAndSaveCert, createRootCA, generateCertificate as generateCert } from '@stacksjs/tlsx'
10
10
  import { log } from './logger'
11
11
  import { config } from './config'
12
+ import {
13
+ MACOS_CA_TRUST_FLAGS,
14
+ MACOS_SYSTEM_KEYCHAIN,
15
+ getMacosLoginKeychainPath,
16
+ isRootCaFingerprintInKeychains,
17
+ isRootCaTrustedForSsl,
18
+ pruneStaleRootCas,
19
+ trustRootCaForBrowsers,
20
+ } from './macos-trust'
12
21
  import { debugLog, execSudoSync, getPrimaryDomain, isMultiProxyConfig, isMultiProxyOptions, isSingleProxyOptions, isValidRootCA, safeDeleteFile } from './utils'
13
22
 
23
+ export {
24
+ MACOS_CA_TRUST_FLAGS,
25
+ MACOS_SYSTEM_KEYCHAIN,
26
+ RPX_ROOT_CA_COMMON_NAME,
27
+ getMacosLoginKeychainPath,
28
+ getMacosTrustKeychains,
29
+ isRootCaFingerprintInKeychains,
30
+ isRootCaTrustedForSsl,
31
+ listCertSha256HashesByCommonName,
32
+ pruneStaleRootCas,
33
+ trustRootCaForBrowsers,
34
+ } from './macos-trust'
35
+
36
+ export {
37
+ certIncludesSanHostnames,
38
+ normalizeSha256Fingerprint,
39
+ parseSha256HashesFromSecurityListing,
40
+ readCertCommonName,
41
+ readCertSha256Fingerprint,
42
+ verifyHttpsChain,
43
+ } from './cert-inspect'
44
+
14
45
  let cachedSSLConfig: { key: string, cert: string, ca?: string } | null = null
15
46
 
16
47
  // Canonical filenames for the shared Root CA. The CA is a singleton across all
@@ -34,6 +65,21 @@ export function getRootCAPaths(basePath: string): RootCAPaths {
34
65
  }
35
66
  }
36
67
 
68
+ /** Paths for the shared multi-host daemon cert under `~/.stacks/ssl`. */
69
+ export function getSharedDaemonCertPaths(sslDir: string): {
70
+ certPath: string
71
+ keyPath: string
72
+ caCertPath: string
73
+ rootCA: RootCAPaths
74
+ } {
75
+ return {
76
+ certPath: join(sslDir, 'rpx.localhost.crt'),
77
+ keyPath: join(sslDir, 'rpx.localhost.key'),
78
+ caCertPath: join(sslDir, 'rpx.localhost.ca.crt'),
79
+ rootCA: getRootCAPaths(sslDir),
80
+ }
81
+ }
82
+
37
83
  /**
38
84
  * Load a previously-persisted Root CA (cert + private key). Returns `null` if
39
85
  * either file is missing or unreadable, signalling that a fresh CA needs to be
@@ -230,9 +276,12 @@ export async function loadSSLConfig(options: ProxyOption): Promise<SSLConfig | n
230
276
  /**
231
277
  * Force trust a certificate - exposing for direct use
232
278
  */
233
- export async function forceTrustCertificate(certPath: string): Promise<boolean> {
279
+ export async function forceTrustCertificate(
280
+ certPath: string,
281
+ options?: { serverName?: string, verbose?: boolean },
282
+ ): Promise<boolean> {
234
283
  if (process.platform === 'darwin')
235
- return forceTrustCertificateMacOS(certPath)
284
+ return forceTrustCertificateMacOS(certPath, options)
236
285
 
237
286
  if (process.platform === 'linux') {
238
287
  try {
@@ -270,28 +319,24 @@ export async function forceTrustCertificate(certPath: string): Promise<boolean>
270
319
  * Force trust a certificate on macOS using direct security command
271
320
  * This function first checks if the certificate is already trusted to avoid unnecessary sudo prompts
272
321
  */
273
- async function forceTrustCertificateMacOS(certPath: string): Promise<boolean> {
322
+ async function forceTrustCertificateMacOS(
323
+ certPath: string,
324
+ options?: { serverName?: string, verbose?: boolean },
325
+ ): Promise<boolean> {
274
326
  if (process.platform !== 'darwin')
275
327
  return false
276
328
 
329
+ const serverName = options?.serverName ?? 'rpx.localhost'
330
+ const verbose = options?.verbose ?? false
331
+
277
332
  try {
278
- // First check if already trusted to avoid unnecessary sudo prompts
279
- const alreadyTrusted = await isCertTrusted(certPath, { verbose: false, regenerateUntrustedCerts: true })
280
- if (alreadyTrusted) {
281
- debugLog('ssl', 'Certificate is already trusted, skipping trust operation', false)
333
+ if (isRootCaTrustedForSsl(certPath, serverName, { verbose })) {
334
+ debugLog('ssl', 'Root CA already trusted for SSL, skipping trust operation', verbose)
282
335
  return true
283
336
  }
284
337
 
285
- debugLog('ssl', 'Trusting certificate via macOS security command', false)
286
-
287
- // Use execSudoSync which handles SUDO_PASSWORD from env
288
- try {
289
- execSudoSync(`security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`)
290
- return true
291
- }
292
- catch {
293
- return false
294
- }
338
+ debugLog('ssl', 'Trusting Root CA for browsers (login + system keychains)', verbose)
339
+ return trustRootCaForBrowsers(certPath, { serverName, verbose })
295
340
  }
296
341
  catch {
297
342
  return false
@@ -299,10 +344,11 @@ async function forceTrustCertificateMacOS(certPath: string): Promise<boolean> {
299
344
  }
300
345
 
301
346
  export async function generateCertificate(options: ProxyOptions): Promise<void> {
302
- if (cachedSSLConfig) {
347
+ if (cachedSSLConfig && !(options as { forceRegenerate?: boolean }).forceRegenerate) {
303
348
  debugLog('ssl', 'Using cached SSL configuration', options.verbose)
304
349
  return
305
350
  }
351
+ clearSslConfigCache()
306
352
 
307
353
  // Get all unique domains from the configuration
308
354
  const domains: string[] = isMultiProxyOptions(options)
@@ -403,7 +449,13 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
403
449
  // We install only the Root CA — host certs derive their trust from it.
404
450
  if (process.platform === 'darwin') {
405
451
  try {
406
- execSudoSync(`security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${rootCAPaths.caCertPath}"`)
452
+ pruneStaleRootCas({ caPath: rootCAPaths.caCertPath, verbose: options.verbose })
453
+ const loginKeychain = getMacosLoginKeychainPath()
454
+ try {
455
+ execSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${loginKeychain}" "${rootCAPaths.caCertPath}"`, { stdio: 'ignore' })
456
+ }
457
+ catch { /* login keychain optional */ }
458
+ execSudoSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"`)
407
459
  if (options.verbose)
408
460
  log.success('Successfully added Root CA to system trust store')
409
461
 
@@ -413,7 +465,7 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
413
465
  const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
414
466
  const scriptContent = `#!/bin/bash
415
467
  echo "Trusting RPX Root CA"
416
- sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${rootCAPaths.caCertPath}"
468
+ sudo security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"
417
469
  echo "Root CA trusted! Please restart your browser."
418
470
  echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
419
471
  `
@@ -426,7 +478,7 @@ echo "If you still see certificate warnings, type 'thisisunsafe' on the warning
426
478
  const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
427
479
  const scriptContent = `#!/bin/bash
428
480
  echo "Trusting RPX Root CA"
429
- sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${rootCAPaths.caCertPath}"
481
+ sudo security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"
430
482
  echo "Root CA trusted! Please restart your browser."
431
483
  echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
432
484
  `
@@ -539,6 +591,11 @@ export function getSSLConfig(): { key: string, cert: string, ca?: string } | nul
539
591
  return cachedSSLConfig
540
592
  }
541
593
 
594
+ /** Clear in-process TLS cache so the next generate/load picks up new files on disk. */
595
+ export function clearSslConfigCache(): void {
596
+ cachedSSLConfig = null
597
+ }
598
+
542
599
  // needs to accept the options
543
600
  export async function checkExistingCertificates(options?: ProxyOptions): Promise<SSLConfig | null> {
544
601
  if (!options)
@@ -569,10 +626,15 @@ export async function checkExistingCertificates(options?: ProxyOptions): Promise
569
626
  const flagValue = options.regenerateUntrustedCerts
570
627
  const shouldCheckTrust = hasFlag ? flagValue !== false : true
571
628
  debugLog('ssl', `Trust check: hasFlag=${hasFlag}, flagValue=${flagValue}, shouldCheckTrust=${shouldCheckTrust}`, options.verbose)
572
- const certIsTrusted = shouldCheckTrust ? await isCertTrusted(sslConfig.certPath, options) : true
573
-
574
- if (!certIsTrusted) {
575
- debugLog('ssl', 'Certificate exists but is not trusted, will regenerate', options.verbose)
629
+ // Trust anchor is the shared Root CA host certs are not installed individually.
630
+ const sslDir = sslConfig.basePath || join(homedir(), '.stacks', 'ssl')
631
+ const rootCAPaths = getRootCAPaths(sslDir)
632
+ const caIsTrusted = shouldCheckTrust
633
+ ? await isCertTrusted(rootCAPaths.caCertPath, options)
634
+ : true
635
+
636
+ if (!caIsTrusted) {
637
+ debugLog('ssl', 'Root CA exists but is not trusted, will regenerate', options.verbose)
576
638
  // Don't attempt to trust here - let generateCertificate handle it in one place
577
639
  // This avoids multiple sudo prompts
578
640
  return null
@@ -711,39 +773,18 @@ export async function cleanupCertificates(domain: string, verbose?: boolean): Pr
711
773
  * Checks if a certificate is trusted by the system (macOS only for now)
712
774
  * If options.regenerateUntrustedCerts is false, always returns true (skips trust check)
713
775
  */
714
- export async function isCertTrusted(certPath: string, options?: { verbose?: boolean, regenerateUntrustedCerts?: boolean }): Promise<boolean> {
776
+ export async function isCertTrusted(
777
+ certPath: string,
778
+ options?: { verbose?: boolean, regenerateUntrustedCerts?: boolean, serverName?: string },
779
+ ): Promise<boolean> {
715
780
  try {
716
781
  debugLog('ssl', `Checking if certificate is trusted: ${certPath}`, options?.verbose)
717
782
 
718
783
  // Different check methods per platform
719
784
  if (process.platform === 'darwin') {
720
- // On macOS, use the security command to check if the cert is trusted
721
- try {
722
- // Get certificate fingerprint
723
- const certFingerprint = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`).toString().trim()
724
- const fingerprintValue = certFingerprint.split('=')[1]?.trim() || ''
725
-
726
- if (!fingerprintValue) {
727
- debugLog('ssl', 'Could not extract certificate fingerprint', options?.verbose)
728
- return false
729
- }
730
-
731
- // Check if the fingerprint exists in the system keychain and is trusted
732
- const keychainOutput = execSync(`security find-certificate -a -Z -p | openssl x509 -noout -fingerprint -sha256`).toString()
733
-
734
- // If the fingerprint is found in the trusted certs, consider it trusted
735
- if (keychainOutput.includes(fingerprintValue)) {
736
- debugLog('ssl', 'Certificate fingerprint found in system keychain', options?.verbose)
737
- return true
738
- }
739
-
740
- debugLog('ssl', 'Certificate fingerprint not found in system keychain', options?.verbose)
741
- return false
742
- }
743
- catch (error) {
744
- debugLog('ssl', `Error checking certificate trust: ${error}`, options?.verbose)
745
- return false
746
- }
785
+ if (options?.serverName)
786
+ return isRootCaTrustedForSsl(certPath, options.serverName, { verbose: options.verbose })
787
+ return isRootCaFingerprintInKeychains(certPath, { verbose: options.verbose })
747
788
  }
748
789
  else if (process.platform === 'win32') {
749
790
  // On Windows, use PowerShell to check the certificate store
package/src/index.ts CHANGED
@@ -13,13 +13,38 @@ export {
13
13
  export {
14
14
  checkExistingCertificates,
15
15
  cleanupCertificates,
16
+ clearSslConfigCache,
16
17
  forceTrustCertificate,
17
18
  generateCertificate,
19
+ getRootCAPaths,
20
+ getSharedDaemonCertPaths,
18
21
  httpsConfig,
19
22
  isCertTrusted,
20
23
  loadSSLConfig,
21
24
  } from './https'
22
25
 
26
+ export {
27
+ MACOS_CA_TRUST_FLAGS,
28
+ MACOS_SYSTEM_KEYCHAIN,
29
+ RPX_ROOT_CA_COMMON_NAME,
30
+ getMacosLoginKeychainPath,
31
+ getMacosTrustKeychains,
32
+ isRootCaFingerprintInKeychains,
33
+ isRootCaTrustedForSsl,
34
+ listCertSha256HashesByCommonName,
35
+ pruneStaleRootCas,
36
+ trustRootCaForBrowsers,
37
+ } from './macos-trust'
38
+
39
+ export {
40
+ certIncludesSanHostnames,
41
+ normalizeSha256Fingerprint,
42
+ parseSha256HashesFromSecurityListing,
43
+ readCertCommonName,
44
+ readCertSha256Fingerprint,
45
+ verifyHttpsChain,
46
+ } from './cert-inspect'
47
+
23
48
  export { DefaultPortManager, findAvailablePort, isPortInUse, portManager } from './port-manager'
24
49
 
25
50
  export {
@@ -36,6 +61,34 @@ export {
36
61
 
37
62
  export type { RegistryEntry, WatchHandle, WatchOptions } from './registry'
38
63
 
64
+ export {
65
+ DNS_PORT,
66
+ RPX_RESOLVER_MARKER,
67
+ contentLooksLikeRpxResolver,
68
+ isDnsServerRunning,
69
+ reconcileStaleDevelopmentDns,
70
+ removeLegacyTldResolvers,
71
+ removeResolver,
72
+ resolverFilePath,
73
+ setupDevelopmentDns,
74
+ setupResolver,
75
+ startDnsServer,
76
+ stopDnsServer,
77
+ syncDevelopmentDnsFromRegistry,
78
+ tearDownDevelopmentDns,
79
+ } from './dns'
80
+
81
+ export type { DevelopmentDnsOptions } from './dns'
82
+
83
+ export {
84
+ DNS_STATE_VERSION,
85
+ LEGACY_TLD_RESOLVER_LABELS,
86
+ devDomainsFromHosts,
87
+ normalizeDevDomain,
88
+ resolverBasenameForDomain,
89
+ resolverBasenamesForDomains,
90
+ } from './dns-state'
91
+
39
92
  export {
40
93
  acquireDaemonLock,
41
94
  defaultDaemonSpawnCommand,
@@ -44,6 +97,7 @@ export {
44
97
  getDaemonRpxDir,
45
98
  isDaemonRunning,
46
99
  readDaemonPid,
100
+ reconcileDevelopmentDnsOnIdle,
47
101
  releaseDaemonLock,
48
102
  runDaemon,
49
103
  stopDaemon,
@@ -0,0 +1,175 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { readCertSha256Fingerprint } from './cert-inspect'
5
+ import { debugLog, execSudoSync } from './utils'
6
+
7
+ /** Chrome/Edge need SSL + basic trust policies — plain trustRoot often leaves "trust settings: 0". */
8
+ export const MACOS_CA_TRUST_FLAGS = '-d -r trustRoot -p ssl -p basic'
9
+
10
+ export const MACOS_SYSTEM_KEYCHAIN = '/Library/Keychains/System.keychain'
11
+
12
+ /** Default CN label for the shared rpx Root CA in macOS keychain listings. */
13
+ export const RPX_ROOT_CA_COMMON_NAME = 'rpx.localhost'
14
+
15
+ export function getMacosLoginKeychainPath(): string {
16
+ return join(homedir(), 'Library/Keychains/login.keychain-db')
17
+ }
18
+
19
+ export function getMacosTrustKeychains(): string[] {
20
+ return [MACOS_SYSTEM_KEYCHAIN, getMacosLoginKeychainPath()]
21
+ }
22
+
23
+ export interface ListCertsByCommonNameOptions {
24
+ keychain: string
25
+ commonName?: string
26
+ }
27
+
28
+ export function listCertSha256HashesByCommonName(
29
+ keychain: string,
30
+ commonName: string = RPX_ROOT_CA_COMMON_NAME,
31
+ ): string[] {
32
+ const listing = execSync(
33
+ `security find-certificate -a -c "${commonName}" -Z "${keychain}" 2>/dev/null || true`,
34
+ { encoding: 'utf8' },
35
+ )
36
+ const hashes: string[] = []
37
+ for (const line of listing.split('\n')) {
38
+ const match = line.match(/SHA-256 hash:\s*([A-F0-9]+)/i)
39
+ if (match)
40
+ hashes.push(match[1]!.toUpperCase())
41
+ }
42
+ return hashes
43
+ }
44
+
45
+ export interface PruneStaleRootCasOptions {
46
+ caPath: string
47
+ commonName?: string
48
+ keychains?: string[]
49
+ verbose?: boolean
50
+ }
51
+
52
+ /**
53
+ * Remove older rpx Root CA copies from keychains, keeping only the fingerprint
54
+ * that matches the on-disk `caPath` file.
55
+ */
56
+ export function pruneStaleRootCas(options: PruneStaleRootCasOptions): void {
57
+ if (process.platform !== 'darwin')
58
+ return
59
+
60
+ const keep = readCertSha256Fingerprint(options.caPath)
61
+ if (!keep)
62
+ return
63
+
64
+ const commonName = options.commonName ?? RPX_ROOT_CA_COMMON_NAME
65
+ const keychains = options.keychains ?? getMacosTrustKeychains()
66
+
67
+ for (const keychain of keychains) {
68
+ for (const hash of listCertSha256HashesByCommonName(keychain, commonName)) {
69
+ if (hash === keep)
70
+ continue
71
+ try {
72
+ if (keychain.startsWith('/Library'))
73
+ execSudoSync(`security delete-certificate -Z ${hash} "${keychain}"`)
74
+ else
75
+ execSync(`security delete-certificate -Z ${hash} "${keychain}"`, { stdio: 'ignore' })
76
+ debugLog('ssl', `Removed stale Root CA ${hash} from ${keychain}`, options.verbose)
77
+ }
78
+ catch { /* already removed */ }
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * True when the Root CA is trusted for SSL to `serverName` (macOS verify-cert).
85
+ * On other platforms, falls back to fingerprint presence in trust stores.
86
+ */
87
+ export function isRootCaTrustedForSsl(
88
+ caPath: string,
89
+ serverName: string,
90
+ options?: { verbose?: boolean },
91
+ ): boolean {
92
+ if (process.platform !== 'darwin')
93
+ return isRootCaFingerprintInKeychains(caPath, options)
94
+
95
+ try {
96
+ const out = execSync(
97
+ `security verify-cert -c "${caPath}" -s "${serverName}" -l -L -R ssl 2>&1`,
98
+ { encoding: 'utf8' },
99
+ )
100
+ const ok = out.includes('successful')
101
+ debugLog('ssl', `verify-cert ${serverName}: ${ok ? 'trusted' : 'not trusted'}`, options?.verbose)
102
+ return ok
103
+ }
104
+ catch {
105
+ return false
106
+ }
107
+ }
108
+
109
+ export function isRootCaFingerprintInKeychains(
110
+ caPath: string,
111
+ options?: { verbose?: boolean },
112
+ ): boolean {
113
+ const fp = readCertSha256Fingerprint(caPath)
114
+ if (!fp)
115
+ return false
116
+
117
+ for (const keychain of getMacosTrustKeychains()) {
118
+ try {
119
+ const listing = execSync(`security find-certificate -a -Z "${keychain}" 2>/dev/null || true`, { encoding: 'utf8' })
120
+ for (const line of listing.split('\n')) {
121
+ if (line.toUpperCase().includes('SHA-256')) {
122
+ const lineFp = line.split('=').pop()!.replace(/SHA-256\s+hash:\s*/gi, '').replace(/:/g, '').trim().toUpperCase()
123
+ if (lineFp === fp) {
124
+ debugLog('ssl', `Root CA fingerprint found in ${keychain}`, options?.verbose)
125
+ return true
126
+ }
127
+ }
128
+ }
129
+ }
130
+ catch { /* try next keychain */ }
131
+ }
132
+
133
+ return false
134
+ }
135
+
136
+ export interface TrustRootCaForBrowsersOptions {
137
+ serverName: string
138
+ commonName?: string
139
+ verbose?: boolean
140
+ }
141
+
142
+ /**
143
+ * Install the Root CA into login + system keychains with SSL/basic policies,
144
+ * pruning stale copies first. Returns true when SSL verification succeeds or
145
+ * the cert fingerprint is present in a keychain.
146
+ */
147
+ export function trustRootCaForBrowsers(
148
+ caPath: string,
149
+ options: TrustRootCaForBrowsersOptions,
150
+ ): boolean {
151
+ if (process.platform !== 'darwin')
152
+ return false
153
+
154
+ const serverName = options.serverName
155
+ pruneStaleRootCas({ caPath, commonName: options.commonName, verbose: options.verbose })
156
+
157
+ const loginKeychain = getMacosLoginKeychainPath()
158
+ try {
159
+ execSync(
160
+ `security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${loginKeychain}" "${caPath}"`,
161
+ { stdio: 'ignore' },
162
+ )
163
+ }
164
+ catch { /* may already exist — re-apply trust below */ }
165
+
166
+ try {
167
+ execSudoSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${caPath}"`)
168
+ }
169
+ catch {
170
+ return false
171
+ }
172
+
173
+ return isRootCaTrustedForSsl(caPath, serverName, { verbose: options.verbose })
174
+ || isRootCaFingerprintInKeychains(caPath, { verbose: options.verbose })
175
+ }
package/src/start.ts CHANGED
@@ -877,10 +877,10 @@ export function startProxy(options: ProxyOption): void {
877
877
  }
878
878
 
879
879
  if (isCustomDomain) {
880
- import('./dns').then(({ startDnsServer, setupResolver }) => {
881
- startDnsServer([targetDomain], mergedOptions.verbose).then((started) => {
880
+ import('./dns').then(({ setupDevelopmentDns }) => {
881
+ setupDevelopmentDns({ domains: [targetDomain], verbose: mergedOptions.verbose }).then((started) => {
882
882
  if (started) {
883
- setupResolver(mergedOptions.verbose, [targetDomain]).then(() => {
883
+ Promise.resolve().then(() => {
884
884
  if (mergedOptions.verbose) {
885
885
  if (reservedTlds.includes(tld)) {
886
886
  log.success(`DNS server started for .${tld} domains`)
@@ -1152,10 +1152,9 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
1152
1152
  }
1153
1153
 
1154
1154
  if (process.platform === 'darwin' && customDomains.length > 0) {
1155
- const { startDnsServer, setupResolver } = await import('./dns')
1156
- const dnsStarted = await startDnsServer(customDomains, verbose)
1155
+ const { setupDevelopmentDns } = await import('./dns')
1156
+ const dnsStarted = await setupDevelopmentDns({ domains: customDomains, verbose })
1157
1157
  if (dnsStarted) {
1158
- await setupResolver(verbose, customDomains)
1159
1158
  if (verbose) {
1160
1159
  const hasReservedOnly = uniqueTlds.every((t): t is string => !!t && reservedTlds.includes(t as string))
1161
1160
  if (hasReservedOnly) {
@@ -1177,9 +1176,8 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
1177
1176
 
1178
1177
  try {
1179
1178
  // Stop DNS server
1180
- const { stopDnsServer, removeResolver } = await import('./dns')
1181
- stopDnsServer(mergedOptions.verbose)
1182
- await removeResolver(mergedOptions.verbose)
1179
+ const { tearDownDevelopmentDns } = await import('./dns')
1180
+ await tearDownDevelopmentDns({ verbose: mergedOptions.verbose })
1183
1181
  }
1184
1182
  catch (err) {
1185
1183
  debugLog('cleanup', `Error stopping DNS server: ${err}`, mergedOptions.verbose)
@@ -1 +0,0 @@
1
- import{a as X,d as F}from"./chunk-jpf41gb9.js";import Q from"node:dgram";var $=15353;function _(k){return{id:k.readUInt16BE(0),flags:k.readUInt16BE(2),qdcount:k.readUInt16BE(4),ancount:k.readUInt16BE(6),nscount:k.readUInt16BE(8),arcount:k.readUInt16BE(10)}}function C(k,z){let A=[],j=z;while(!0){let B=k[j];if(B===0){j++;break}if((B&192)===192){let G=k.readUInt16BE(j)&16383,{name:E}=C(k,G);A.push(E),j+=2;break}j++,A.push(k.subarray(j,j+B).toString("ascii")),j+=B}return{name:A.join("."),newOffset:j}}function H(k,z){let{name:A,newOffset:j}=C(k,z),B=k.readUInt16BE(j),G=k.readUInt16BE(j+2);return{question:{name:A,type:B,class:G},newOffset:j+4}}function U(k){let z=k.split("."),A=[];for(let j of z)A.push(Buffer.from([j.length])),A.push(Buffer.from(j,"ascii"));return A.push(Buffer.from([0])),Buffer.concat(A)}function T(k,z,A){let j=[],B=Buffer.alloc(12);B.writeUInt16BE(k,0),B.writeUInt16BE(33152,2),B.writeUInt16BE(1,4),B.writeUInt16BE(1,6),B.writeUInt16BE(0,8),B.writeUInt16BE(0,10),j.push(B),j.push(U(z.name));let G=Buffer.alloc(4);G.writeUInt16BE(z.type,0),G.writeUInt16BE(z.class,2),j.push(G),j.push(U(z.name));let E=Buffer.alloc(10);if(E.writeUInt16BE(z.type,0),E.writeUInt16BE(1,2),E.writeUInt32BE(300,4),z.type===1){E.writeUInt16BE(4,8),j.push(E);let K=A.split(".").map((M)=>Number.parseInt(M,10));j.push(Buffer.from(K))}else if(z.type===28)E.writeUInt16BE(16,8),j.push(E),j.push(Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]));else return B.writeUInt16BE(33155,2),B.writeUInt16BE(0,6),Buffer.concat([B,U(z.name),G]);return Buffer.concat(j)}function I(k,z){let A=[],j=Buffer.alloc(12);j.writeUInt16BE(k,0),j.writeUInt16BE(33155,2),j.writeUInt16BE(1,4),j.writeUInt16BE(0,6),j.writeUInt16BE(0,8),j.writeUInt16BE(0,10),A.push(j),A.push(U(z.name));let B=Buffer.alloc(4);return B.writeUInt16BE(z.type,0),B.writeUInt16BE(z.class,2),A.push(B),Buffer.concat(A)}var J=null,Z=new Set;async function R(k,z){if(J)return F("dns","DNS server already running",z),!0;return Z=new Set(k.map((A)=>A.toLowerCase())),new Promise((A)=>{J=Q.createSocket("udp4"),J.on("error",(j)=>{if(F("dns",`DNS server error: ${j.message}`,z),j.message.includes("EACCES")||j.message.includes("permission"))F("dns","DNS server requires root privileges to bind to port 53",z);J?.close(),J=null,A(!1)}),J.on("message",(j,B)=>{try{let G=_(j),{question:E}=H(j,12);F("dns",`Query for ${E.name} type ${E.type} from ${B.address}`,z);let K=E.name.toLowerCase(),M=!1;for(let Y of Z)if(K===Y||K.endsWith(`.${Y}`)){M=!0;break}let W;if(M&&(E.type===1||E.type===28))W=T(G.id,E,"127.0.0.1"),F("dns",`Responding with localhost for ${E.name}`,z);else W=I(G.id,E),F("dns",`NXDOMAIN for ${E.name}`,z);J?.send(W,B.port,B.address)}catch(G){F("dns",`Error processing DNS query: ${G}`,z)}}),J.on("listening",()=>{let j=J?.address();F("dns",`DNS server listening on ${j?.address}:${j?.port}`,z),A(!0)});try{J.bind($,"127.0.0.1")}catch(j){F("dns",`Failed to bind DNS server: ${j}`,z),A(!1)}})}function O(k){if(J)F("dns","Stopping DNS server",k),J.close(),J=null}function w(){return J!==null}function x(k){let z=new Set;for(let A of k){let j=A.split(".");if(j.length>=2)z.add(j[j.length-1])}return Array.from(z)}var V=new Set;async function D(k){if(process.platform!=="darwin")return;let{execSudoSync:z,getSudoPassword:A}=await import("./chunk-qcdcnadb.js");if(!A()){F("dns","Cannot flush DNS cache without SUDO_PASSWORD",k);return}try{z("dscacheutil -flushcache"),z("killall -HUP mDNSResponder 2>/dev/null || true"),F("dns","DNS cache flushed",k)}catch(B){F("dns",`Could not flush DNS cache: ${B}`,k)}}async function y(k,z){if(process.platform!=="darwin")return F("dns","Resolver setup only needed on macOS",k),!0;let{execSudoSync:A,getSudoPassword:j}=await import("./chunk-qcdcnadb.js");if(!j())return F("dns","SUDO_PASSWORD not set, cannot create resolver files",k),!1;let G=z?x(z):["test"];try{for(let E of G){if(V.has(E))continue;let K=`bash -c 'mkdir -p /etc/resolver && echo -e "nameserver 127.0.0.1\\nport ${$}" > /etc/resolver/${E}'`;A(K),V.add(E),F("dns",`Created /etc/resolver/${E} for .${E} TLD`,k)}return await D(k),!0}catch(E){return F("dns",`Failed to create resolver file: ${E}`,k),!1}}async function L(k){if(process.platform!=="darwin")return;let{execSudoSync:z,getSudoPassword:A}=await import("./chunk-qcdcnadb.js");try{if(A()){for(let B of V)z(`rm -f /etc/resolver/${B}`),F("dns",`Removed /etc/resolver/${B}`,k);V.clear()}}catch(j){F("dns",`Failed to remove resolver files: ${j}`,k)}}export{O as stopDnsServer,R as startDnsServer,y as setupResolver,L as removeResolver,w as isDnsServerRunning};
@@ -1 +0,0 @@
1
- import{b as a,c as b,d as c,e as d,f as e,g as f,h as g,i as h,j as i,k as j,l as k,m as l,n as m,o as n}from"./chunk-jpf41gb9.js";export{e as safeStringify,n as safeDeleteFile,m as resolvePathRewrite,d as redactSensitive,g as isValidRootCA,k as isSingleProxyOptions,l as isSingleProxyConfig,j as isMultiProxyOptions,i as isMultiProxyConfig,a as getSudoPassword,h as getPrimaryDomain,f as extractHostname,b as execSudoSync,c as debugLog};