unplugin-https-reverse-proxy 4.0.3 → 4.1.0-beta.2

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 (53) hide show
  1. package/README.md +30 -6
  2. package/dist/{chunk-MD5CVZP7.js → chunk-7CA3EIIX.js} +682 -224
  3. package/dist/{chunk-RSBXFJ3Z.js → chunk-C5A7XLKL.js} +9 -2
  4. package/dist/{chunk-2P7SUCQ3.js → chunk-EY3HXBJ4.js} +0 -26
  5. package/dist/chunk-RKHJN6KY.js +53 -0
  6. package/dist/esbuild.cjs +790 -303
  7. package/dist/esbuild.js +4 -3
  8. package/dist/hmr-sse-bridge.cjs +12 -4
  9. package/dist/hmr-sse-bridge.d.cts +4 -3
  10. package/dist/hmr-sse-bridge.d.ts +4 -3
  11. package/dist/hmr-sse-bridge.js +5 -3
  12. package/dist/index.cjs +790 -303
  13. package/dist/index.js +4 -3
  14. package/dist/nuxt.cjs +787 -292
  15. package/dist/nuxt.js +5 -9
  16. package/dist/privileged-helper.cjs +668 -0
  17. package/dist/privileged-helper.d.cts +2 -0
  18. package/dist/privileged-helper.d.ts +2 -0
  19. package/dist/privileged-helper.js +641 -0
  20. package/dist/rollup.cjs +790 -303
  21. package/dist/rollup.js +4 -3
  22. package/dist/rspack.cjs +790 -303
  23. package/dist/rspack.js +4 -3
  24. package/dist/utils.cjs +0 -28
  25. package/dist/utils.d.cts +1 -3
  26. package/dist/utils.d.ts +1 -3
  27. package/dist/utils.js +1 -5
  28. package/dist/vite.cjs +790 -303
  29. package/dist/vite.js +4 -3
  30. package/dist/webpack.cjs +790 -303
  31. package/dist/webpack.js +4 -3
  32. package/package.json +1 -4
  33. package/src/caddy/caddyfile.ts +1 -0
  34. package/src/caddy/index.ts +183 -110
  35. package/src/caddy/privileged.ts +118 -0
  36. package/src/caddy/utils.ts +0 -43
  37. package/src/hmr-sse-bridge.ts +14 -3
  38. package/src/index.ts +4 -18
  39. package/src/nuxt.ts +1 -6
  40. package/src/privileged/bootstrap.ts +174 -0
  41. package/src/privileged/client.ts +287 -0
  42. package/src/privileged/entry.ts +45 -0
  43. package/src/privileged/hosts-file.ts +142 -0
  44. package/src/privileged/launch.ts +193 -0
  45. package/src/privileged/ports.ts +45 -0
  46. package/src/privileged/protocol.ts +111 -0
  47. package/src/privileged/server.ts +465 -0
  48. package/src/privileged/session.ts +114 -0
  49. package/src/privileged/validation.ts +38 -0
  50. package/src/privileged/windows-elevation.ts +80 -0
  51. package/src/privileged-helper.ts +109 -0
  52. package/src/utils.ts +0 -28
  53. package/src/host/index.ts +0 -52
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import c from 'picocolors'
7
7
  import { createUnplugin } from 'unplugin'
8
8
  import { CaddyInstant } from './caddy'
9
9
  import { hmrSseBridgeClientScript, setupHmrSseBridge } from './hmr-sse-bridge'
10
- import { consola, generateQrcode, getIpv4List, isAdmin, once } from './utils'
10
+ import { consola, generateQrcode, getIpv4List, once } from './utils'
11
11
 
12
12
  let config: ResolvedConfig
13
13
 
@@ -96,10 +96,6 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
96
96
  if (!enable)
97
97
  return
98
98
 
99
- if (!isAdmin()) {
100
- consola.warn('please run as administrator')
101
- return
102
- }
103
99
  if (!target) {
104
100
  consola.fail('please provide target')
105
101
  return
@@ -123,17 +119,17 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
123
119
 
124
120
  server.printUrls = () => vitePrintUrls(options, source, target, base, _printUrls)
125
121
 
126
- // Setup SSE bridge for iOS Safari WSS workaround
122
+ // Setup SSE bridge for macOS/iOS Safari WSS workaround
127
123
  if (options.https) {
128
124
  setupHmrSseBridge(server)
129
125
  hmrSseBridgeActive = true
130
- consola.info('HMR SSE bridge enabled for iOS Safari WSS workaround')
126
+ consola.info('HMR SSE bridge enabled for macOS/iOS Safari WSS workaround')
131
127
  }
132
128
  },
133
129
  transformIndexHtml() {
134
130
  if (!hmrSseBridgeActive)
135
131
  return []
136
- // Inject client-side script to patch WebSocket on iOS Safari
132
+ // Inject client-side script to patch WebSocket on macOS/iOS Safari
137
133
  return [{
138
134
  tag: 'script',
139
135
  children: hmrSseBridgeClientScript,
@@ -153,11 +149,6 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
153
149
  if (!enable)
154
150
  return
155
151
 
156
- if (!isAdmin()) {
157
- consola.warn('please run as administrator')
158
- return
159
- }
160
-
161
152
  if (!target) {
162
153
  consola.fail('please provide target')
163
154
  return
@@ -231,11 +222,6 @@ export const unpluginFactory: UnpluginFactory<Options> = options => ({
231
222
  if (!enable)
232
223
  return
233
224
 
234
- if (!isAdmin()) {
235
- consola.warn('please run as administrator')
236
- return
237
- }
238
-
239
225
  if (!target) {
240
226
  consola.fail('please provide target')
241
227
  return
package/src/nuxt.ts CHANGED
@@ -2,7 +2,7 @@ import type { Options } from './types'
2
2
  import process from 'node:process'
3
3
  import { defineNuxtModule } from '@nuxt/kit'
4
4
  import { vitePrintUrls } from '.'
5
- import { consola, isAdmin } from './utils'
5
+ import { consola } from './utils'
6
6
  import '@nuxt/schema'
7
7
 
8
8
  export interface ModuleOptions extends Options {
@@ -26,11 +26,6 @@ export default defineNuxtModule<ModuleOptions>({
26
26
  if (!enable)
27
27
  return
28
28
 
29
- if (!isAdmin()) {
30
- consola.warn('please run as administrator')
31
- return
32
- }
33
-
34
29
  if (!target) {
35
30
  consola.fail('please provide target')
36
31
  return
@@ -0,0 +1,174 @@
1
+ import type { Socket } from 'node:net'
2
+ import type { ClientHandshake } from './protocol'
3
+ import type { HelperDeps, HelperRuntime } from './server'
4
+ import { Buffer } from 'node:buffer'
5
+ import { chmodSync, chownSync, rmSync } from 'node:fs'
6
+ import { createServer } from 'node:net'
7
+ import process from 'node:process'
8
+ import { attachSocket, createHelperRuntime, createNodeHelperDeps } from './server'
9
+
10
+ export interface HelperBootstrapOptions {
11
+ socketPath: string
12
+ token: string
13
+ hostsPath?: string
14
+ /** Caddy binary; fixed here so the client never names an executable. */
15
+ caddyBin: string
16
+ /** Caddyfile to load; fixed for the same reason. */
17
+ caddyConfigPath: string
18
+ caddyCwd?: string
19
+ caddyEnv?: Record<string, string>
20
+ /**
21
+ * Owner of the socket file. The helper runs as root while its client runs as
22
+ * the developer, so the socket must be handed back or the client cannot
23
+ * connect to it.
24
+ */
25
+ ownerUid?: number
26
+ ownerGid?: number
27
+ /** Caddy storage directory, handed back to the owner when Caddy stops. */
28
+ caddyStoragePath?: string
29
+ deps?: HelperDeps
30
+ log?: (message: string) => void
31
+ }
32
+
33
+ export interface RunningHelper {
34
+ runtime: HelperRuntime
35
+ close: () => Promise<void>
36
+ }
37
+
38
+ const HANDSHAKE_TIMEOUT_MS = 120_000
39
+
40
+ /**
41
+ * Runs the privileged side of the plugin: listens on a local socket, accepts
42
+ * only a client presenting the shared token, then serves until that client
43
+ * disconnects. Losing the client is the cleanup trigger, so a crashed or
44
+ * `SIGKILL`ed plugin still leaves hosts and Caddy restored.
45
+ */
46
+ export async function startHelper(options: HelperBootstrapOptions): Promise<RunningHelper> {
47
+ const { socketPath, token } = options
48
+ const log = options.log ?? ((message: string) => process.stdout.write(`[uhrp-helper] ${message}\n`))
49
+ const deps = options.deps ?? createNodeHelperDeps({
50
+ hostsPath: options.hostsPath,
51
+ log,
52
+ caddyBin: options.caddyBin,
53
+ caddyConfigPath: options.caddyConfigPath,
54
+ caddyCwd: options.caddyCwd,
55
+ caddyEnv: options.caddyEnv,
56
+ caddyStoragePath: options.caddyStoragePath,
57
+ storageOwner: typeof options.ownerUid === 'number'
58
+ ? { uid: options.ownerUid, gid: options.ownerGid ?? options.ownerUid }
59
+ : undefined,
60
+ })
61
+ const runtime = createHelperRuntime(deps)
62
+
63
+ // A previous session that was killed cannot have cleaned up its entries; do
64
+ // it now, before any client attaches.
65
+ await runtime.sweepStaleEntries().catch(() => {})
66
+
67
+ try {
68
+ rmSync(socketPath, { force: true })
69
+ }
70
+ catch {}
71
+
72
+ const connections = new Set<Socket>()
73
+
74
+ const server = createServer((socket) => {
75
+ let buffer = ''
76
+ let authenticated = false
77
+ connections.add(socket)
78
+ socket.on('close', () => connections.delete(socket))
79
+
80
+ const timer = setTimeout(() => {
81
+ if (!authenticated)
82
+ socket.destroy()
83
+ }, HANDSHAKE_TIMEOUT_MS)
84
+ timer.unref?.()
85
+
86
+ const onData = (chunk: Buffer): void => {
87
+ buffer += chunk.toString('utf8')
88
+ const index = buffer.indexOf('\n')
89
+ if (index < 0)
90
+ return
91
+
92
+ const line = buffer.slice(0, index)
93
+ const rest = buffer.slice(index + 1)
94
+
95
+ let parsed: Partial<ClientHandshake>
96
+ try {
97
+ parsed = JSON.parse(line)
98
+ }
99
+ catch {
100
+ socket.destroy()
101
+ return
102
+ }
103
+
104
+ if (parsed.type !== 'handshake' || typeof parsed.token !== 'string') {
105
+ socket.destroy()
106
+ return
107
+ }
108
+
109
+ clearTimeout(timer)
110
+ socket.removeListener('data', onData)
111
+
112
+ if (parsed.token !== token) {
113
+ socket.end(`${JSON.stringify({ type: 'handshake', ok: false, reason: 'invalid token' })}\n`)
114
+ return
115
+ }
116
+
117
+ authenticated = true
118
+ socket.write(`${JSON.stringify({ type: 'handshake', ok: true, pid: process.pid })}\n`)
119
+ attachSocket(socket, runtime, log)
120
+
121
+ // Requests pipelined behind the handshake belong to the attached client.
122
+ if (rest.trim() !== '')
123
+ socket.emit('data', Buffer.from(rest))
124
+ }
125
+
126
+ socket.on('data', onData)
127
+ socket.on('error', () => {})
128
+ })
129
+
130
+ await new Promise<void>((resolve, reject) => {
131
+ server.once('error', reject)
132
+ server.listen(socketPath, () => resolve())
133
+ })
134
+
135
+ try {
136
+ // The client is not root: hand the socket back to the developer and keep it
137
+ // private to them. Without the chown, `connect()` fails with EACCES and the
138
+ // client only sees a silent retry loop.
139
+ if (typeof options.ownerUid === 'number') {
140
+ chownSync(socketPath, options.ownerUid, options.ownerGid ?? options.ownerUid)
141
+ chmodSync(socketPath, 0o600)
142
+ }
143
+ else {
144
+ chmodSync(socketPath, 0o600)
145
+ }
146
+ }
147
+ catch (error) {
148
+ log(`could not set socket ownership: ${(error as Error).message}`)
149
+ }
150
+
151
+ return {
152
+ runtime,
153
+ async close() {
154
+ // Closing the listener alone would wait for clients to hang up first;
155
+ // shutdown has to drop the open connections as well.
156
+ for (const socket of connections)
157
+ socket.destroy()
158
+ connections.clear()
159
+ await new Promise<void>((resolve) => {
160
+ server.close(() => resolve())
161
+ })
162
+ },
163
+ }
164
+ }
165
+
166
+ /** Sockets left behind by a killed helper cannot be reused; remove them first. */
167
+ export function removeStaleSocket(socketPath: string): void {
168
+ if (process.platform === 'win32')
169
+ return
170
+ try {
171
+ rmSync(socketPath, { force: true })
172
+ }
173
+ catch {}
174
+ }
@@ -0,0 +1,287 @@
1
+ import type { Buffer } from 'node:buffer'
2
+ import type { Socket } from 'node:net'
3
+ import type {
4
+ ClientHandshake,
5
+ HelperEvent,
6
+ HelperMessage,
7
+ HelperRequest,
8
+ RpcId,
9
+ ServerHandshake,
10
+ } from './protocol'
11
+ import { connect } from 'node:net'
12
+ import process from 'node:process'
13
+ import { decodeMessage, serializeMessage, splitLines } from './protocol'
14
+
15
+ /**
16
+ * Environment variables the privileged helper needs so Caddy behaves like the
17
+ * user's own process (PKI location, proxy settings) without `sudo -E`.
18
+ */
19
+ export const FORWARDED_ENV_KEYS = [
20
+ 'HOME',
21
+ 'USER',
22
+ 'LOGNAME',
23
+ 'TMPDIR',
24
+ 'HTTP_PROXY',
25
+ 'HTTPS_PROXY',
26
+ 'http_proxy',
27
+ 'https_proxy',
28
+ 'NO_PROXY',
29
+ 'no_proxy',
30
+ ] as const
31
+
32
+ export function collectForwardedEnv(source: NodeJS.ProcessEnv = process.env): Record<string, string> {
33
+ const result: Record<string, string> = {}
34
+ for (const key of FORWARDED_ENV_KEYS) {
35
+ const value = source[key]
36
+ if (typeof value === 'string' && value !== '')
37
+ result[key] = value
38
+ }
39
+ return result
40
+ }
41
+
42
+ export interface HelperResults {
43
+ 'ping': { pid: number }
44
+ 'hosts.add': void
45
+ 'hosts.remove': void
46
+ 'port.free': { killed: number[] }
47
+ 'caddy.start': { pid?: number }
48
+ 'caddy.stop': void
49
+ 'ca.trust': { trusted: boolean, output: string }
50
+ }
51
+
52
+ export class HelperConnectionError extends Error {
53
+ constructor(message: string) {
54
+ super(message)
55
+ this.name = 'HelperConnectionError'
56
+ }
57
+ }
58
+
59
+ export interface PrivilegedClient {
60
+ readonly connected: boolean
61
+ call: <M extends keyof HelperResults>(method: M, ...params: HelperParams<M>) => Promise<HelperResults[M]>
62
+ onEvent: (listener: (event: HelperEvent) => void) => () => void
63
+ close: () => Promise<void>
64
+ }
65
+
66
+ type HelperParams<M extends keyof HelperResults>
67
+ = M extends 'hosts.add' | 'hosts.remove' ? [params: { ip: string, host: string }]
68
+ : M extends 'port.free' ? [params: { port: number }]
69
+ : []
70
+
71
+ interface PendingCall {
72
+ resolve: (value: unknown) => void
73
+ reject: (error: Error) => void
74
+ timer: ReturnType<typeof setTimeout>
75
+ }
76
+
77
+ export interface ConnectOptions {
78
+ socketPath: string
79
+ token: string
80
+ /** Total time to wait for the helper to accept the connection. */
81
+ timeoutMs?: number
82
+ retryIntervalMs?: number
83
+ callTimeoutMs?: number
84
+ }
85
+
86
+ function openSocket(socketPath: string): Promise<Socket> {
87
+ return new Promise<Socket>((resolve, reject) => {
88
+ const socket = connect(socketPath)
89
+
90
+ function onConnect(): void {
91
+ socket.removeListener('error', onError)
92
+ resolve(socket)
93
+ }
94
+
95
+ function onError(error: Error): void {
96
+ socket.removeListener('connect', onConnect)
97
+ reject(error)
98
+ }
99
+
100
+ socket.once('error', onError)
101
+ socket.once('connect', onConnect)
102
+ })
103
+ }
104
+
105
+ /**
106
+ * Connects to an already-running helper and performs the token handshake.
107
+ * Retries because the helper may still be starting while sudo asks for a password.
108
+ */
109
+ export async function connectClient(options: ConnectOptions): Promise<PrivilegedClient> {
110
+ const {
111
+ socketPath,
112
+ token,
113
+ timeoutMs = 120_000,
114
+ retryIntervalMs = 100,
115
+ callTimeoutMs = 30_000,
116
+ } = options
117
+
118
+ const deadline = Date.now() + timeoutMs
119
+ let lastError: Error | undefined
120
+
121
+ while (Date.now() < deadline) {
122
+ try {
123
+ return await handshake(socketPath, token, callTimeoutMs)
124
+ }
125
+ catch (error) {
126
+ lastError = error as Error
127
+
128
+ // A permission or protocol error will not resolve by waiting, and
129
+ // retrying hides the real cause behind a silent hang.
130
+ const code = (lastError as NodeJS.ErrnoException).code
131
+ if (code === 'EACCES' || lastError instanceof HelperConnectionError)
132
+ throw lastError
133
+
134
+ await new Promise(resolve => setTimeout(resolve, retryIntervalMs))
135
+ }
136
+ }
137
+
138
+ throw new HelperConnectionError(
139
+ `privileged helper is not reachable at ${socketPath}: ${lastError?.message ?? 'timed out'}`,
140
+ )
141
+ }
142
+
143
+ function handshake(socketPath: string, token: string, callTimeoutMs: number): Promise<PrivilegedClient> {
144
+ return new Promise<PrivilegedClient>((resolve, reject) => {
145
+ void openSocket(socketPath).then((socket) => {
146
+ let buffer = ''
147
+ let settled = false
148
+
149
+ const fail = (error: Error): void => {
150
+ if (settled)
151
+ return
152
+ settled = true
153
+ socket.destroy()
154
+ reject(error)
155
+ }
156
+
157
+ const onData = (chunk: Buffer): void => {
158
+ if (settled)
159
+ return
160
+
161
+ const split = splitLines(buffer, chunk.toString('utf8'))
162
+ buffer = split.rest
163
+ const line = split.lines[0]
164
+ if (line === undefined)
165
+ return
166
+
167
+ let hello: ServerHandshake
168
+ try {
169
+ hello = JSON.parse(line) as ServerHandshake
170
+ }
171
+ catch (error) {
172
+ return fail(new HelperConnectionError(`invalid handshake reply: ${(error as Error).message}`))
173
+ }
174
+
175
+ if (hello.type !== 'handshake')
176
+ return fail(new HelperConnectionError('helper did not answer the handshake'))
177
+
178
+ if (!hello.ok)
179
+ return fail(new HelperConnectionError(hello.reason ?? 'helper rejected the handshake'))
180
+
181
+ settled = true
182
+ socket.removeListener('data', onData)
183
+ socket.removeListener('error', onError)
184
+ const remaining = split.lines.slice(1)
185
+ resolve(createClient(socket, [split.rest, ...remaining].filter(Boolean).join('\n'), callTimeoutMs))
186
+ }
187
+
188
+ function onError(error: Error): void {
189
+ fail(error)
190
+ }
191
+
192
+ socket.on('data', onData)
193
+ socket.on('error', onError)
194
+
195
+ const greeting: ClientHandshake = { type: 'handshake', token }
196
+ socket.write(serializeMessage(greeting))
197
+ }, reject)
198
+ })
199
+ }
200
+
201
+ function createClient(socket: Socket, initialBuffer: string, callTimeoutMs: number): PrivilegedClient {
202
+ let buffer = initialBuffer
203
+ let connected = true
204
+ let nextId: RpcId = 1
205
+
206
+ const pending = new Map<RpcId, PendingCall>()
207
+ const listeners = new Set<(event: HelperEvent) => void>()
208
+
209
+ socket.on('data', (chunk: Buffer) => {
210
+ const split = splitLines(buffer, chunk.toString('utf8'))
211
+ buffer = split.rest
212
+
213
+ for (const line of split.lines) {
214
+ const decoded = decodeMessage(line)
215
+ if (!decoded.ok)
216
+ continue
217
+
218
+ const message: HelperMessage = decoded.message
219
+ if (!('id' in message)) {
220
+ for (const listener of listeners)
221
+ listener(message)
222
+ continue
223
+ }
224
+
225
+ const entry = pending.get(message.id)
226
+ if (!entry)
227
+ continue
228
+
229
+ pending.delete(message.id)
230
+ clearTimeout(entry.timer)
231
+
232
+ if (message.ok)
233
+ entry.resolve(message.result)
234
+ else
235
+ entry.reject(new Error(message.error.message))
236
+ }
237
+ })
238
+
239
+ const disconnect = (): void => {
240
+ if (!connected)
241
+ return
242
+ connected = false
243
+ for (const [id, entry] of pending) {
244
+ clearTimeout(entry.timer)
245
+ pending.delete(id)
246
+ entry.reject(new HelperConnectionError('privileged helper connection closed'))
247
+ }
248
+ }
249
+
250
+ socket.on('close', disconnect)
251
+ socket.on('error', disconnect)
252
+
253
+ function call(method: keyof HelperResults, params?: unknown): Promise<unknown> {
254
+ if (!connected)
255
+ return Promise.reject(new HelperConnectionError('privileged helper is not connected'))
256
+
257
+ const id = nextId++
258
+ const request = { id, method, params } as unknown as HelperRequest
259
+
260
+ return new Promise<unknown>((resolve, reject) => {
261
+ const timer = setTimeout(() => {
262
+ pending.delete(id)
263
+ reject(new HelperConnectionError(`privileged helper did not answer "${method}" in time`))
264
+ }, callTimeoutMs)
265
+
266
+ pending.set(id, { resolve, reject, timer })
267
+ socket.write(serializeMessage(request))
268
+ })
269
+ }
270
+
271
+ return {
272
+ get connected() {
273
+ return connected
274
+ },
275
+ call: call as PrivilegedClient['call'],
276
+ onEvent(listener) {
277
+ listeners.add(listener)
278
+ return () => {
279
+ listeners.delete(listener)
280
+ }
281
+ },
282
+ async close() {
283
+ disconnect()
284
+ socket.destroy()
285
+ },
286
+ }
287
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ export interface HelperEntry {
6
+ /**
7
+ * Executable started through sudo. This is the node runtime, so the caller
8
+ * only has to append `args` and the helper's own flags.
9
+ */
10
+ entry: string
11
+ /** Script (and loader) arguments that follow the runtime. */
12
+ args: string[]
13
+ }
14
+
15
+ export interface ResolveHelperEntryOptions {
16
+ /** Directory of the running bundle: `dist` in published builds, `src` from source. */
17
+ moduleDir: string
18
+ /** Node executable used to run a compiled helper. */
19
+ nodeBin?: string
20
+ exists?: (path: string) => boolean
21
+ }
22
+
23
+ /**
24
+ * Locates the helper entry next to the running module.
25
+ *
26
+ * Published builds ship a compiled `privileged-helper.js` and start it with
27
+ * node; running from the repository uses `tsx` so the TypeScript source works
28
+ * without a build step.
29
+ */
30
+ export function resolveHelperEntry(options: ResolveHelperEntryOptions): HelperEntry {
31
+ const { moduleDir, nodeBin = process.execPath, exists = existsSync } = options
32
+
33
+ for (const name of ['privileged-helper.js', 'privileged-helper.cjs', 'privileged-helper.mjs']) {
34
+ const candidate = join(moduleDir, name)
35
+ if (exists(candidate))
36
+ return { entry: nodeBin, args: [candidate] }
37
+ }
38
+
39
+ const source = join(moduleDir, 'privileged-helper.ts')
40
+ const tsxCli = join(moduleDir, '../node_modules/tsx/dist/cli.mjs')
41
+ if (exists(source) && exists(tsxCli))
42
+ return { entry: nodeBin, args: [tsxCli, source] }
43
+
44
+ throw new Error(`privileged helper entry not found in ${moduleDir}. Build the package first.`)
45
+ }