@stacksjs/rpx 0.11.19 → 0.11.21

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 (49) hide show
  1. package/dist/acme-challenge.d.ts +13 -0
  2. package/dist/auth.d.ts +21 -0
  3. package/dist/bin/cli.js +252 -200
  4. package/dist/cert-inspect.d.ts +5 -3
  5. package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
  6. package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
  7. package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
  8. package/dist/chunk-qs36t2rf.js +224 -0
  9. package/dist/daemon.d.ts +4 -1
  10. package/dist/https.d.ts +1 -0
  11. package/dist/index.d.ts +33 -1
  12. package/dist/index.js +7 -7
  13. package/dist/proxy-handler.d.ts +18 -1
  14. package/dist/proxy-pool.d.ts +5 -0
  15. package/dist/redirect.d.ts +40 -0
  16. package/dist/registry.d.ts +2 -1
  17. package/dist/site-resolver.d.ts +106 -0
  18. package/dist/site-splash.d.ts +19 -0
  19. package/dist/site-supervisor.d.ts +56 -0
  20. package/dist/start.d.ts +31 -8
  21. package/dist/types.d.ts +64 -0
  22. package/package.json +6 -7
  23. package/dist/chunk-rs8gqpax.js +0 -174
  24. package/src/cert-inspect.ts +0 -69
  25. package/src/colors.ts +0 -13
  26. package/src/config.ts +0 -45
  27. package/src/daemon-runner.ts +0 -180
  28. package/src/daemon.ts +0 -1209
  29. package/src/dns-state.ts +0 -116
  30. package/src/dns.ts +0 -568
  31. package/src/host-match.ts +0 -52
  32. package/src/host-routes.ts +0 -147
  33. package/src/hosts.ts +0 -283
  34. package/src/https.ts +0 -905
  35. package/src/index.ts +0 -161
  36. package/src/logger.ts +0 -19
  37. package/src/macos-trust.ts +0 -175
  38. package/src/on-demand.ts +0 -264
  39. package/src/origin-guard.ts +0 -127
  40. package/src/port-manager.ts +0 -183
  41. package/src/process-manager.ts +0 -164
  42. package/src/proxy-handler.ts +0 -387
  43. package/src/proxy-pool.ts +0 -1003
  44. package/src/registry.ts +0 -366
  45. package/src/sni.ts +0 -93
  46. package/src/start.ts +0 -1421
  47. package/src/static-files.ts +0 -201
  48. package/src/types.ts +0 -267
  49. package/src/utils.ts +0 -243
package/src/dns-state.ts DELETED
@@ -1,116 +0,0 @@
1
- import * as fsp from 'node:fs/promises'
2
- import { homedir } from 'node:os'
3
- import * as path from 'node:path'
4
-
5
- export const DNS_STATE_VERSION = 1 as const
6
- export const RPX_DNS_STATE_FILE = 'dns-state.json'
7
-
8
- /** Single-label /etc/resolver files created by older rpx versions (whole-TLD hijack). */
9
- export const LEGACY_TLD_RESOLVER_LABELS = [
10
- 'com',
11
- 'test',
12
- 'dev',
13
- 'app',
14
- 'page',
15
- 'local',
16
- 'localhost',
17
- 'example',
18
- 'invalid',
19
- ] as const
20
-
21
- export interface DnsState {
22
- version: typeof DNS_STATE_VERSION
23
- /** Basenames under /etc/resolver/ (e.g. `postline.test`, not `test`). */
24
- resolvers: string[]
25
- domains: string[]
26
- ownerPid: number | null
27
- updatedAt: string
28
- }
29
-
30
- export function defaultRpxDir(): string {
31
- return path.join(homedir(), '.stacks', 'rpx')
32
- }
33
-
34
- export function getDnsStatePath(rpxDir: string = defaultRpxDir()): string {
35
- return path.join(rpxDir, RPX_DNS_STATE_FILE)
36
- }
37
-
38
- export async function loadDnsState(rpxDir: string = defaultRpxDir()): Promise<DnsState | null> {
39
- try {
40
- const raw = await fsp.readFile(getDnsStatePath(rpxDir), 'utf8')
41
- const parsed = JSON.parse(raw) as Partial<DnsState>
42
- if (parsed.version !== DNS_STATE_VERSION || !Array.isArray(parsed.resolvers))
43
- return null
44
- return {
45
- version: DNS_STATE_VERSION,
46
- resolvers: parsed.resolvers.filter((r): r is string => typeof r === 'string'),
47
- domains: Array.isArray(parsed.domains)
48
- ? parsed.domains.filter((d): d is string => typeof d === 'string')
49
- : [],
50
- ownerPid: typeof parsed.ownerPid === 'number' ? parsed.ownerPid : null,
51
- updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
52
- }
53
- }
54
- catch (err) {
55
- if ((err as NodeJS.ErrnoException).code === 'ENOENT')
56
- return null
57
- throw err
58
- }
59
- }
60
-
61
- export async function saveDnsState(rpxDir: string, state: DnsState): Promise<void> {
62
- await fsp.mkdir(rpxDir, { recursive: true })
63
- await fsp.writeFile(getDnsStatePath(rpxDir), `${JSON.stringify(state, null, 2)}\n`, 'utf8')
64
- }
65
-
66
- export async function clearDnsState(rpxDir: string): Promise<void> {
67
- await fsp.rm(getDnsStatePath(rpxDir), { force: true })
68
- }
69
-
70
- /**
71
- * Normalize a dev hostname. Returns null for localhost / IPs — those use /etc/hosts only.
72
- */
73
- export function normalizeDevDomain(raw: string): string | null {
74
- const domain = raw.trim().toLowerCase().replace(/\.$/, '')
75
- if (!domain || domain.includes('127.0.0.1'))
76
- return null
77
- if (domain === 'localhost' || domain.endsWith('.localhost'))
78
- return null
79
- if (/^\d{1,3}(\.\d{1,3}){3}$/.test(domain))
80
- return null
81
- return domain
82
- }
83
-
84
- /**
85
- * macOS resolver basename for a dev domain. Uses the registrable base (last two labels)
86
- * so `api.postline.test` and `postline.test` share one `/etc/resolver/postline.test` file.
87
- */
88
- export function resolverBasenameForDomain(raw: string): string | null {
89
- const domain = normalizeDevDomain(raw)
90
- if (!domain)
91
- return null
92
- const parts = domain.split('.')
93
- if (parts.length < 2)
94
- return null
95
- return parts.slice(-2).join('.')
96
- }
97
-
98
- export function resolverBasenamesForDomains(domains: string[]): string[] {
99
- const basenames = new Set<string>()
100
- for (const raw of domains) {
101
- const basename = resolverBasenameForDomain(raw)
102
- if (basename)
103
- basenames.add(basename)
104
- }
105
- return Array.from(basenames).sort()
106
- }
107
-
108
- export function devDomainsFromHosts(hosts: string[]): string[] {
109
- const out = new Set<string>()
110
- for (const raw of hosts) {
111
- const domain = normalizeDevDomain(raw)
112
- if (domain)
113
- out.add(domain)
114
- }
115
- return Array.from(out).sort()
116
- }
package/src/dns.ts DELETED
@@ -1,568 +0,0 @@
1
- /**
2
- * Local development DNS for macOS.
3
- *
4
- * Uses domain-scoped `/etc/resolver/<base-domain>` files (never whole-TLD hijacks like
5
- * `/etc/resolver/com`) so real sites keep working when the rpx DNS server is down.
6
- */
7
- import dgram from 'node:dgram'
8
- import * as fsp from 'node:fs/promises'
9
- import * as path from 'node:path'
10
- import * as process from 'node:process'
11
- import type { RegistryEntry } from './registry'
12
- import { getDaemonRpxDir } from './daemon'
13
- import {
14
- type DnsState,
15
- DNS_STATE_VERSION,
16
- devDomainsFromHosts,
17
- LEGACY_TLD_RESOLVER_LABELS,
18
- loadDnsState,
19
- resolverBasenamesForDomains,
20
- saveDnsState,
21
- clearDnsState,
22
- } from './dns-state'
23
- import { isPidAlive } from './registry'
24
- import { debugLog } from './utils'
25
-
26
- /** High port — does not require root. */
27
- export const DNS_PORT = 15353
28
-
29
- export const RPX_RESOLVER_MARKER = '# managed-by: rpx'
30
-
31
- const MACOS_RESOLVER_DIR = '/etc/resolver'
32
-
33
- export interface DevelopmentDnsOptions {
34
- domains: string[]
35
- rpxDir?: string
36
- verbose?: boolean
37
- /** Defaults to `process.pid` — stored so stale state can be reconciled after crashes. */
38
- ownerPid?: number
39
- }
40
-
41
- let dnsServer: dgram.Socket | null = null
42
- let configuredDomains: Set<string> = new Set()
43
-
44
- // ---------------------------------------------------------------------------
45
- // DNS UDP server
46
- // ---------------------------------------------------------------------------
47
-
48
- interface DnsHeader {
49
- id: number
50
- flags: number
51
- qdcount: number
52
- ancount: number
53
- nscount: number
54
- arcount: number
55
- }
56
-
57
- interface DnsQuestion {
58
- name: string
59
- type: number
60
- class: number
61
- }
62
-
63
- function parseHeader(buffer: Buffer): DnsHeader {
64
- return {
65
- id: buffer.readUInt16BE(0),
66
- flags: buffer.readUInt16BE(2),
67
- qdcount: buffer.readUInt16BE(4),
68
- ancount: buffer.readUInt16BE(6),
69
- nscount: buffer.readUInt16BE(8),
70
- arcount: buffer.readUInt16BE(10),
71
- }
72
- }
73
-
74
- function parseName(buffer: Buffer, offset: number): { name: string, newOffset: number } {
75
- const labels: string[] = []
76
- let currentOffset = offset
77
-
78
- while (true) {
79
- const length = buffer[currentOffset]
80
-
81
- if (length === 0) {
82
- currentOffset++
83
- break
84
- }
85
-
86
- if ((length & 0xC0) === 0xC0) {
87
- const pointer = buffer.readUInt16BE(currentOffset) & 0x3FFF
88
- const { name } = parseName(buffer, pointer)
89
- labels.push(name)
90
- currentOffset += 2
91
- break
92
- }
93
-
94
- currentOffset++
95
- labels.push(buffer.subarray(currentOffset, currentOffset + length).toString('ascii'))
96
- currentOffset += length
97
- }
98
-
99
- return { name: labels.join('.'), newOffset: currentOffset }
100
- }
101
-
102
- function parseQuestion(buffer: Buffer, offset: number): { question: DnsQuestion, newOffset: number } {
103
- const { name, newOffset } = parseName(buffer, offset)
104
- const type = buffer.readUInt16BE(newOffset)
105
- const qclass = buffer.readUInt16BE(newOffset + 2)
106
-
107
- return {
108
- question: { name, type, class: qclass },
109
- newOffset: newOffset + 4,
110
- }
111
- }
112
-
113
- function encodeName(name: string): Buffer {
114
- const labels = name.split('.')
115
- const parts: Buffer[] = []
116
-
117
- for (const label of labels) {
118
- parts.push(Buffer.from([label.length]))
119
- parts.push(Buffer.from(label, 'ascii'))
120
- }
121
- parts.push(Buffer.from([0]))
122
-
123
- return Buffer.concat(parts)
124
- }
125
-
126
- function buildResponse(queryId: number, question: DnsQuestion, ip: string): Buffer {
127
- const parts: Buffer[] = []
128
-
129
- const header = Buffer.alloc(12)
130
- header.writeUInt16BE(queryId, 0)
131
- header.writeUInt16BE(0x8180, 2)
132
- header.writeUInt16BE(1, 4)
133
- header.writeUInt16BE(1, 6)
134
- header.writeUInt16BE(0, 8)
135
- header.writeUInt16BE(0, 10)
136
- parts.push(header)
137
-
138
- parts.push(encodeName(question.name))
139
- const qtype = Buffer.alloc(4)
140
- qtype.writeUInt16BE(question.type, 0)
141
- qtype.writeUInt16BE(question.class, 2)
142
- parts.push(qtype)
143
-
144
- parts.push(encodeName(question.name))
145
-
146
- const answer = Buffer.alloc(10)
147
- answer.writeUInt16BE(question.type, 0)
148
- answer.writeUInt16BE(1, 2)
149
- answer.writeUInt32BE(300, 4)
150
-
151
- if (question.type === 1) {
152
- answer.writeUInt16BE(4, 8)
153
- parts.push(answer)
154
- const ipParts = ip.split('.').map(p => Number.parseInt(p, 10))
155
- parts.push(Buffer.from(ipParts))
156
- }
157
- else if (question.type === 28) {
158
- answer.writeUInt16BE(16, 8)
159
- parts.push(answer)
160
- parts.push(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]))
161
- }
162
- else {
163
- header.writeUInt16BE(0x8183, 2)
164
- header.writeUInt16BE(0, 6)
165
- return Buffer.concat([header, encodeName(question.name), qtype])
166
- }
167
-
168
- return Buffer.concat(parts)
169
- }
170
-
171
- function buildNxdomainResponse(queryId: number, question: DnsQuestion): Buffer {
172
- const parts: Buffer[] = []
173
-
174
- const header = Buffer.alloc(12)
175
- header.writeUInt16BE(queryId, 0)
176
- header.writeUInt16BE(0x8183, 2)
177
- header.writeUInt16BE(1, 4)
178
- header.writeUInt16BE(0, 6)
179
- header.writeUInt16BE(0, 8)
180
- header.writeUInt16BE(0, 10)
181
- parts.push(header)
182
-
183
- parts.push(encodeName(question.name))
184
- const qtype = Buffer.alloc(4)
185
- qtype.writeUInt16BE(question.type, 0)
186
- qtype.writeUInt16BE(question.class, 2)
187
- parts.push(qtype)
188
-
189
- return Buffer.concat(parts)
190
- }
191
-
192
- export async function startDnsServer(domains: string[], verbose?: boolean): Promise<boolean> {
193
- if (process.platform !== 'darwin')
194
- return false
195
-
196
- const devDomains = devDomainsFromHosts(domains)
197
- if (devDomains.length === 0)
198
- return false
199
-
200
- if (dnsServer) {
201
- for (const d of devDomains)
202
- configuredDomains.add(d)
203
- debugLog('dns', 'DNS server already running — merged domains', verbose)
204
- return true
205
- }
206
-
207
- configuredDomains = new Set(devDomains)
208
-
209
- return new Promise((resolve) => {
210
- dnsServer = dgram.createSocket('udp4')
211
-
212
- dnsServer.on('error', (err) => {
213
- debugLog('dns', `DNS server error: ${err.message}`, verbose)
214
- dnsServer?.close()
215
- dnsServer = null
216
- resolve(false)
217
- })
218
-
219
- dnsServer.on('message', (msg, rinfo) => {
220
- try {
221
- const header = parseHeader(msg)
222
- const { question } = parseQuestion(msg, 12)
223
-
224
- debugLog('dns', `Query for ${question.name} type ${question.type} from ${rinfo.address}`, verbose)
225
-
226
- const domainLower = question.name.toLowerCase()
227
- let shouldHandle = false
228
-
229
- for (const configured of configuredDomains) {
230
- if (domainLower === configured || domainLower.endsWith(`.${configured}`)) {
231
- shouldHandle = true
232
- break
233
- }
234
- }
235
-
236
- let response: Buffer
237
- if (shouldHandle && (question.type === 1 || question.type === 28)) {
238
- response = buildResponse(header.id, question, '127.0.0.1')
239
- debugLog('dns', `Responding with localhost for ${question.name}`, verbose)
240
- }
241
- else {
242
- response = buildNxdomainResponse(header.id, question)
243
- debugLog('dns', `NXDOMAIN for ${question.name}`, verbose)
244
- }
245
-
246
- dnsServer?.send(response, rinfo.port, rinfo.address)
247
- }
248
- catch (err) {
249
- debugLog('dns', `Error processing DNS query: ${err}`, verbose)
250
- }
251
- })
252
-
253
- dnsServer.on('listening', () => {
254
- const address = dnsServer?.address()
255
- debugLog('dns', `DNS server listening on ${address?.address}:${address?.port}`, verbose)
256
- resolve(true)
257
- })
258
-
259
- try {
260
- dnsServer.bind(DNS_PORT, '127.0.0.1')
261
- }
262
- catch (err) {
263
- debugLog('dns', `Failed to bind DNS server: ${err}`, verbose)
264
- resolve(false)
265
- }
266
- })
267
- }
268
-
269
- export function stopDnsServer(verbose?: boolean): void {
270
- if (dnsServer) {
271
- debugLog('dns', 'Stopping DNS server', verbose)
272
- dnsServer.close()
273
- dnsServer = null
274
- configuredDomains = new Set()
275
- }
276
- }
277
-
278
- export function isDnsServerRunning(): boolean {
279
- return dnsServer !== null
280
- }
281
-
282
- /** True when something is answering A queries for `host` on the rpx dev DNS port. */
283
- export async function isRpxDevelopmentDnsAnswering(host: string, timeoutMs = 500): Promise<boolean> {
284
- const domain = host.trim().toLowerCase().replace(/\.$/, '')
285
- if (!domain)
286
- return false
287
-
288
- return new Promise((resolve) => {
289
- const socket = dgram.createSocket('udp4')
290
- const timer = setTimeout(() => {
291
- socket.close()
292
- resolve(false)
293
- }, timeoutMs)
294
-
295
- socket.on('message', (msg) => {
296
- clearTimeout(timer)
297
- socket.close()
298
- try {
299
- const header = parseHeader(msg)
300
- resolve(header.ancount > 0)
301
- }
302
- catch {
303
- resolve(false)
304
- }
305
- })
306
-
307
- socket.on('error', () => {
308
- clearTimeout(timer)
309
- socket.close()
310
- resolve(false)
311
- })
312
-
313
- const query = buildQuery(domain, 1)
314
- socket.send(query, DNS_PORT, '127.0.0.1', (err) => {
315
- if (err) {
316
- clearTimeout(timer)
317
- socket.close()
318
- resolve(false)
319
- }
320
- })
321
- })
322
- }
323
-
324
- function buildQuery(name: string, type: number): Buffer {
325
- const header = Buffer.alloc(12)
326
- header.writeUInt16BE(1, 0)
327
- header.writeUInt16BE(0x0100, 2)
328
- header.writeUInt16BE(1, 4)
329
- const question = Buffer.concat([encodeName(name), Buffer.from([0, type, 0, 1])])
330
- return Buffer.concat([header, question])
331
- }
332
-
333
- // ---------------------------------------------------------------------------
334
- // macOS resolver files
335
- // ---------------------------------------------------------------------------
336
-
337
- function resolverFileContent(): string {
338
- return `${RPX_RESOLVER_MARKER}\nnameserver 127.0.0.1\nport ${DNS_PORT}\n`
339
- }
340
-
341
- export function resolverFilePath(basename: string): string {
342
- return path.join(MACOS_RESOLVER_DIR, basename)
343
- }
344
-
345
- /** True when a resolver file points at the rpx local DNS port. */
346
- export function contentLooksLikeRpxResolver(content: string): boolean {
347
- return content.includes('127.0.0.1') && content.includes(String(DNS_PORT))
348
- }
349
-
350
- async function readResolverFile(basename: string): Promise<string | null> {
351
- try {
352
- return await fsp.readFile(resolverFilePath(basename), 'utf8')
353
- }
354
- catch (err) {
355
- if ((err as NodeJS.ErrnoException).code === 'ENOENT')
356
- return null
357
- throw err
358
- }
359
- }
360
-
361
- async function flushDnsCache(verbose?: boolean): Promise<void> {
362
- if (process.platform !== 'darwin')
363
- return
364
-
365
- const { execSudoSync, getSudoPassword, isProcessElevated } = await import('./utils')
366
-
367
- if (!isProcessElevated() && !getSudoPassword()) {
368
- debugLog('dns', 'Cannot flush DNS cache without SUDO_PASSWORD', verbose)
369
- return
370
- }
371
-
372
- try {
373
- execSudoSync('dscacheutil -flushcache')
374
- execSudoSync('killall -HUP mDNSResponder 2>/dev/null || true')
375
- debugLog('dns', 'DNS cache flushed', verbose)
376
- }
377
- catch (err) {
378
- debugLog('dns', `Could not flush DNS cache: ${err}`, verbose)
379
- }
380
- }
381
-
382
- async function writeResolverFile(basename: string, verbose?: boolean): Promise<void> {
383
- const { execSudoSync } = await import('./utils')
384
- const content = resolverFileContent().replace(/\n/g, '\\n')
385
- const cmd = `bash -c 'mkdir -p ${MACOS_RESOLVER_DIR} && printf "%b" "${content}" > ${resolverFilePath(basename)}'`
386
- execSudoSync(cmd)
387
- debugLog('dns', `Created ${resolverFilePath(basename)}`, verbose)
388
- }
389
-
390
- async function removeResolverFile(basename: string, verbose?: boolean): Promise<void> {
391
- const { execSudoSync } = await import('./utils')
392
- execSudoSync(`rm -f ${resolverFilePath(basename)}`)
393
- debugLog('dns', `Removed ${resolverFilePath(basename)}`, verbose)
394
- }
395
-
396
- /**
397
- * @deprecated Use {@link setupDevelopmentDns}. Domain-scoped resolver files only.
398
- */
399
- export async function setupResolver(verbose?: boolean, domains?: string[]): Promise<boolean> {
400
- return setupDevelopmentDns({ domains: domains ?? [], verbose })
401
- }
402
-
403
- async function installResolvers(basenames: string[], verbose?: boolean): Promise<boolean> {
404
- if (process.platform !== 'darwin')
405
- return true
406
-
407
- const { getSudoPassword, isProcessElevated } = await import('./utils')
408
- if (!isProcessElevated() && !getSudoPassword()) {
409
- debugLog('dns', 'SUDO_PASSWORD not set, cannot create resolver files', verbose)
410
- return false
411
- }
412
-
413
- try {
414
- for (const basename of basenames)
415
- await writeResolverFile(basename, verbose)
416
- await flushDnsCache(verbose)
417
- return true
418
- }
419
- catch (err) {
420
- debugLog('dns', `Failed to create resolver file: ${err}`, verbose)
421
- return false
422
- }
423
- }
424
-
425
- async function uninstallResolvers(basenames: string[], verbose?: boolean): Promise<void> {
426
- if (process.platform !== 'darwin')
427
- return
428
-
429
- const { getSudoPassword, isProcessElevated } = await import('./utils')
430
- if (!isProcessElevated() && !getSudoPassword())
431
- return
432
-
433
- try {
434
- for (const basename of basenames)
435
- await removeResolverFile(basename, verbose)
436
- await flushDnsCache(verbose)
437
- }
438
- catch (err) {
439
- debugLog('dns', `Failed to remove resolver files: ${err}`, verbose)
440
- }
441
- }
442
-
443
- /** Remove legacy whole-TLD resolver files (e.g. `/etc/resolver/com`). */
444
- export async function removeLegacyTldResolvers(verbose?: boolean): Promise<string[]> {
445
- if (process.platform !== 'darwin')
446
- return []
447
-
448
- const removed: string[] = []
449
- for (const label of LEGACY_TLD_RESOLVER_LABELS) {
450
- const content = await readResolverFile(label)
451
- if (content && contentLooksLikeRpxResolver(content)) {
452
- await uninstallResolvers([label], verbose)
453
- removed.push(label)
454
- }
455
- }
456
- return removed
457
- }
458
-
459
- /**
460
- * Start the local DNS server and install domain-scoped macOS resolver files.
461
- */
462
- export async function setupDevelopmentDns(opts: DevelopmentDnsOptions): Promise<boolean> {
463
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
464
- const domains = devDomainsFromHosts(opts.domains)
465
- if (domains.length === 0)
466
- return false
467
-
468
- const basenames = resolverBasenamesForDomains(domains)
469
- let started = await startDnsServer(domains, opts.verbose)
470
- if (!started) {
471
- const probeHost = domains[0]
472
- if (probeHost && await isRpxDevelopmentDnsAnswering(probeHost))
473
- started = true
474
- else
475
- debugLog('dns', 'Dev DNS server not available on 127.0.0.1:15353', opts.verbose)
476
- }
477
-
478
- if (!started)
479
- return false
480
-
481
- const installed = await installResolvers(basenames, opts.verbose)
482
- if (!installed)
483
- return false
484
-
485
- const state: DnsState = {
486
- version: DNS_STATE_VERSION,
487
- resolvers: basenames,
488
- domains,
489
- ownerPid: opts.ownerPid ?? process.pid,
490
- updatedAt: new Date().toISOString(),
491
- }
492
- await saveDnsState(rpxDir, state)
493
- return true
494
- }
495
-
496
- /**
497
- * Sync resolver + DNS state to the current set of registry hosts (daemon mode).
498
- */
499
- export async function syncDevelopmentDnsFromRegistry(
500
- entries: RegistryEntry[],
501
- opts: { rpxDir?: string, verbose?: boolean, ownerPid?: number } = {},
502
- ): Promise<void> {
503
- const domains = entries.map(e => e.to).filter(Boolean)
504
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
505
- const wanted = resolverBasenamesForDomains(domains)
506
- const state = await loadDnsState(rpxDir)
507
- const previous = state?.resolvers ?? []
508
- const toRemove = previous.filter(b => !wanted.includes(b))
509
-
510
- if (toRemove.length > 0)
511
- await uninstallResolvers(toRemove, opts.verbose)
512
-
513
- if (wanted.length === 0) {
514
- stopDnsServer(opts.verbose)
515
- await clearDnsState(rpxDir)
516
- return
517
- }
518
-
519
- await setupDevelopmentDns({
520
- domains,
521
- rpxDir,
522
- verbose: opts.verbose,
523
- ownerPid: opts.ownerPid ?? process.pid,
524
- })
525
- }
526
-
527
- /**
528
- * Stop DNS and remove all resolver files recorded in state (plus legacy TLD files).
529
- */
530
- export async function tearDownDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
531
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
532
- stopDnsServer(opts.verbose)
533
-
534
- const state = await loadDnsState(rpxDir)
535
- const fromState = state?.resolvers ?? []
536
- await uninstallResolvers(fromState, opts.verbose)
537
- await removeLegacyTldResolvers(opts.verbose)
538
- await clearDnsState(rpxDir)
539
- }
540
-
541
- /**
542
- * @deprecated Use {@link tearDownDevelopmentDns}.
543
- */
544
- export async function removeResolver(verbose?: boolean): Promise<void> {
545
- await tearDownDevelopmentDns({ verbose })
546
- }
547
-
548
- /**
549
- * Remove stale DNS overrides left after a crashed dev session or legacy TLD hijacks.
550
- * Safe to call before starting the daemon or `./buddy dev`.
551
- */
552
- export async function reconcileStaleDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
553
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
554
- const state = await loadDnsState(rpxDir)
555
- const ownerAlive = state?.ownerPid != null && isPidAlive(state.ownerPid)
556
-
557
- if (state && !ownerAlive) {
558
- debugLog('dns', `reconcile: owner pid ${state.ownerPid} is gone — tearing down DNS`, opts.verbose)
559
- await tearDownDevelopmentDns(opts)
560
- return
561
- }
562
-
563
- const legacyRemoved = await removeLegacyTldResolvers(opts.verbose)
564
- if (legacyRemoved.length > 0)
565
- debugLog('dns', `reconcile: removed legacy TLD resolvers: ${legacyRemoved.join(', ')}`, opts.verbose)
566
-
567
- await flushDnsCache(opts.verbose)
568
- }