@stacksjs/rpx 0.11.18 → 0.11.20

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 (46) hide show
  1. package/dist/bin/cli.js +225 -210
  2. package/dist/cert-inspect.d.ts +5 -3
  3. package/dist/chunk-0f32jmrb.js +1 -0
  4. package/dist/chunk-bs9e342s.js +1 -0
  5. package/dist/chunk-vxj1ckdm.js +176 -0
  6. package/dist/{chunk-rbgb5fyg.js → chunk-w888yhnp.js} +12 -12
  7. package/dist/daemon.d.ts +17 -8
  8. package/dist/dns.d.ts +2 -0
  9. package/dist/https.d.ts +10 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.js +7 -7
  12. package/dist/logger.d.ts +8 -8
  13. package/dist/proxy-handler.d.ts +1 -1
  14. package/dist/proxy-pool.d.ts +82 -0
  15. package/dist/start.d.ts +31 -8
  16. package/dist/types.d.ts +13 -0
  17. package/dist/utils.d.ts +15 -0
  18. package/package.json +11 -7
  19. package/dist/chunk-9njrhbd4.js +0 -1
  20. package/dist/chunk-a0b9f9fs.js +0 -1
  21. package/dist/chunk-s4tdtq37.js +0 -161
  22. package/src/cert-inspect.ts +0 -69
  23. package/src/colors.ts +0 -13
  24. package/src/config.ts +0 -45
  25. package/src/daemon-runner.ts +0 -180
  26. package/src/daemon.ts +0 -786
  27. package/src/dns-state.ts +0 -116
  28. package/src/dns.ts +0 -509
  29. package/src/host-match.ts +0 -52
  30. package/src/host-routes.ts +0 -147
  31. package/src/hosts.ts +0 -278
  32. package/src/https.ts +0 -862
  33. package/src/index.ts +0 -158
  34. package/src/logger.ts +0 -19
  35. package/src/macos-trust.ts +0 -175
  36. package/src/on-demand.ts +0 -264
  37. package/src/origin-guard.ts +0 -105
  38. package/src/port-manager.ts +0 -183
  39. package/src/process-manager.ts +0 -164
  40. package/src/proxy-handler.ts +0 -315
  41. package/src/registry.ts +0 -366
  42. package/src/sni.ts +0 -93
  43. package/src/start.ts +0 -1421
  44. package/src/static-files.ts +0 -201
  45. package/src/types.ts +0 -256
  46. package/src/utils.ts +0 -210
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,509 +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
- // ---------------------------------------------------------------------------
283
- // macOS resolver files
284
- // ---------------------------------------------------------------------------
285
-
286
- function resolverFileContent(): string {
287
- return `${RPX_RESOLVER_MARKER}\nnameserver 127.0.0.1\nport ${DNS_PORT}\n`
288
- }
289
-
290
- export function resolverFilePath(basename: string): string {
291
- return path.join(MACOS_RESOLVER_DIR, basename)
292
- }
293
-
294
- /** True when a resolver file points at the rpx local DNS port. */
295
- export function contentLooksLikeRpxResolver(content: string): boolean {
296
- return content.includes('127.0.0.1') && content.includes(String(DNS_PORT))
297
- }
298
-
299
- async function readResolverFile(basename: string): Promise<string | null> {
300
- try {
301
- return await fsp.readFile(resolverFilePath(basename), 'utf8')
302
- }
303
- catch (err) {
304
- if ((err as NodeJS.ErrnoException).code === 'ENOENT')
305
- return null
306
- throw err
307
- }
308
- }
309
-
310
- async function flushDnsCache(verbose?: boolean): Promise<void> {
311
- if (process.platform !== 'darwin')
312
- return
313
-
314
- const { execSudoSync, getSudoPassword } = await import('./utils')
315
-
316
- if (!getSudoPassword()) {
317
- debugLog('dns', 'Cannot flush DNS cache without SUDO_PASSWORD', verbose)
318
- return
319
- }
320
-
321
- try {
322
- execSudoSync('dscacheutil -flushcache')
323
- execSudoSync('killall -HUP mDNSResponder 2>/dev/null || true')
324
- debugLog('dns', 'DNS cache flushed', verbose)
325
- }
326
- catch (err) {
327
- debugLog('dns', `Could not flush DNS cache: ${err}`, verbose)
328
- }
329
- }
330
-
331
- async function writeResolverFile(basename: string, verbose?: boolean): Promise<void> {
332
- const { execSudoSync } = await import('./utils')
333
- const content = resolverFileContent().replace(/\n/g, '\\n')
334
- const cmd = `bash -c 'mkdir -p ${MACOS_RESOLVER_DIR} && printf "%b" "${content}" > ${resolverFilePath(basename)}'`
335
- execSudoSync(cmd)
336
- debugLog('dns', `Created ${resolverFilePath(basename)}`, verbose)
337
- }
338
-
339
- async function removeResolverFile(basename: string, verbose?: boolean): Promise<void> {
340
- const { execSudoSync } = await import('./utils')
341
- execSudoSync(`rm -f ${resolverFilePath(basename)}`)
342
- debugLog('dns', `Removed ${resolverFilePath(basename)}`, verbose)
343
- }
344
-
345
- /**
346
- * @deprecated Use {@link setupDevelopmentDns}. Domain-scoped resolver files only.
347
- */
348
- export async function setupResolver(verbose?: boolean, domains?: string[]): Promise<boolean> {
349
- return setupDevelopmentDns({ domains: domains ?? [], verbose })
350
- }
351
-
352
- async function installResolvers(basenames: string[], verbose?: boolean): Promise<boolean> {
353
- if (process.platform !== 'darwin')
354
- return true
355
-
356
- const { getSudoPassword } = await import('./utils')
357
- if (!getSudoPassword()) {
358
- debugLog('dns', 'SUDO_PASSWORD not set, cannot create resolver files', verbose)
359
- return false
360
- }
361
-
362
- try {
363
- for (const basename of basenames)
364
- await writeResolverFile(basename, verbose)
365
- await flushDnsCache(verbose)
366
- return true
367
- }
368
- catch (err) {
369
- debugLog('dns', `Failed to create resolver file: ${err}`, verbose)
370
- return false
371
- }
372
- }
373
-
374
- async function uninstallResolvers(basenames: string[], verbose?: boolean): Promise<void> {
375
- if (process.platform !== 'darwin')
376
- return
377
-
378
- const { getSudoPassword } = await import('./utils')
379
- if (!getSudoPassword())
380
- return
381
-
382
- try {
383
- for (const basename of basenames)
384
- await removeResolverFile(basename, verbose)
385
- await flushDnsCache(verbose)
386
- }
387
- catch (err) {
388
- debugLog('dns', `Failed to remove resolver files: ${err}`, verbose)
389
- }
390
- }
391
-
392
- /** Remove legacy whole-TLD resolver files (e.g. `/etc/resolver/com`). */
393
- export async function removeLegacyTldResolvers(verbose?: boolean): Promise<string[]> {
394
- if (process.platform !== 'darwin')
395
- return []
396
-
397
- const removed: string[] = []
398
- for (const label of LEGACY_TLD_RESOLVER_LABELS) {
399
- const content = await readResolverFile(label)
400
- if (content && contentLooksLikeRpxResolver(content)) {
401
- await uninstallResolvers([label], verbose)
402
- removed.push(label)
403
- }
404
- }
405
- return removed
406
- }
407
-
408
- /**
409
- * Start the local DNS server and install domain-scoped macOS resolver files.
410
- */
411
- export async function setupDevelopmentDns(opts: DevelopmentDnsOptions): Promise<boolean> {
412
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
413
- const domains = devDomainsFromHosts(opts.domains)
414
- if (domains.length === 0)
415
- return false
416
-
417
- const basenames = resolverBasenamesForDomains(domains)
418
- const started = await startDnsServer(domains, opts.verbose)
419
- if (!started)
420
- return false
421
-
422
- const installed = await installResolvers(basenames, opts.verbose)
423
- if (!installed)
424
- return false
425
-
426
- const state: DnsState = {
427
- version: DNS_STATE_VERSION,
428
- resolvers: basenames,
429
- domains,
430
- ownerPid: opts.ownerPid ?? process.pid,
431
- updatedAt: new Date().toISOString(),
432
- }
433
- await saveDnsState(rpxDir, state)
434
- return true
435
- }
436
-
437
- /**
438
- * Sync resolver + DNS state to the current set of registry hosts (daemon mode).
439
- */
440
- export async function syncDevelopmentDnsFromRegistry(
441
- entries: RegistryEntry[],
442
- opts: { rpxDir?: string, verbose?: boolean, ownerPid?: number } = {},
443
- ): Promise<void> {
444
- const domains = entries.map(e => e.to).filter(Boolean)
445
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
446
- const wanted = resolverBasenamesForDomains(domains)
447
- const state = await loadDnsState(rpxDir)
448
- const previous = state?.resolvers ?? []
449
- const toRemove = previous.filter(b => !wanted.includes(b))
450
-
451
- if (toRemove.length > 0)
452
- await uninstallResolvers(toRemove, opts.verbose)
453
-
454
- if (wanted.length === 0) {
455
- stopDnsServer(opts.verbose)
456
- await clearDnsState(rpxDir)
457
- return
458
- }
459
-
460
- await setupDevelopmentDns({
461
- domains,
462
- rpxDir,
463
- verbose: opts.verbose,
464
- ownerPid: opts.ownerPid ?? process.pid,
465
- })
466
- }
467
-
468
- /**
469
- * Stop DNS and remove all resolver files recorded in state (plus legacy TLD files).
470
- */
471
- export async function tearDownDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
472
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
473
- stopDnsServer(opts.verbose)
474
-
475
- const state = await loadDnsState(rpxDir)
476
- const fromState = state?.resolvers ?? []
477
- await uninstallResolvers(fromState, opts.verbose)
478
- await removeLegacyTldResolvers(opts.verbose)
479
- await clearDnsState(rpxDir)
480
- }
481
-
482
- /**
483
- * @deprecated Use {@link tearDownDevelopmentDns}.
484
- */
485
- export async function removeResolver(verbose?: boolean): Promise<void> {
486
- await tearDownDevelopmentDns({ verbose })
487
- }
488
-
489
- /**
490
- * Remove stale DNS overrides left after a crashed dev session or legacy TLD hijacks.
491
- * Safe to call before starting the daemon or `./buddy dev`.
492
- */
493
- export async function reconcileStaleDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
494
- const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
495
- const state = await loadDnsState(rpxDir)
496
- const ownerAlive = state?.ownerPid != null && isPidAlive(state.ownerPid)
497
-
498
- if (state && !ownerAlive) {
499
- debugLog('dns', `reconcile: owner pid ${state.ownerPid} is gone — tearing down DNS`, opts.verbose)
500
- await tearDownDevelopmentDns(opts)
501
- return
502
- }
503
-
504
- const legacyRemoved = await removeLegacyTldResolvers(opts.verbose)
505
- if (legacyRemoved.length > 0)
506
- debugLog('dns', `reconcile: removed legacy TLD resolvers: ${legacyRemoved.join(', ')}`, opts.verbose)
507
-
508
- await flushDnsCache(opts.verbose)
509
- }
package/src/host-match.ts DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * Host-based route matching with wildcard support.
3
- *
4
- * The routing table is keyed by host pattern. A pattern is either an exact
5
- * hostname (`api.example.com`) or a wildcard (`*.example.com`). Lookup prefers
6
- * an exact match, then the most-specific (deepest-suffix) wildcard.
7
- *
8
- * Kept dependency-free and pure so it's reusable from both the daemon and the
9
- * in-process multi-proxy path, and trivially unit-testable.
10
- */
11
-
12
- export function isWildcardPattern(pattern: string): boolean {
13
- return pattern.startsWith('*.')
14
- }
15
-
16
- /**
17
- * True if `hostname` matches the wildcard `pattern` (`*.suffix`). A wildcard
18
- * matches exactly one or more leading labels — `*.example.com` matches
19
- * `a.example.com` and `a.b.example.com`, but NOT the bare apex `example.com`.
20
- */
21
- export function matchesWildcard(hostname: string, pattern: string): boolean {
22
- if (!isWildcardPattern(pattern))
23
- return false
24
- const suffix = pattern.slice(1) // '*.example.com' → '.example.com'
25
- return hostname.length > suffix.length && hostname.endsWith(suffix)
26
- }
27
-
28
- /**
29
- * Find the route value for `hostname` in a host-keyed map. Exact match wins;
30
- * otherwise the matching wildcard with the longest (most-specific) suffix wins.
31
- * Returns `undefined` when nothing matches.
32
- */
33
- export function matchHost<T>(table: Map<string, T>, hostname: string): T | undefined {
34
- const exact = table.get(hostname)
35
- if (exact !== undefined)
36
- return exact
37
-
38
- let best: T | undefined
39
- let bestLen = -1
40
- for (const [pattern, value] of table) {
41
- if (!isWildcardPattern(pattern))
42
- continue
43
- if (matchesWildcard(hostname, pattern)) {
44
- const len = pattern.length - 1 // length of the matched suffix
45
- if (len > bestLen) {
46
- bestLen = len
47
- best = value
48
- }
49
- }
50
- }
51
- return best
52
- }