bunqueue-dashboard 0.0.35 → 0.0.36
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/README.md +18 -11
- package/agent/backup/processExecutor.ts +90 -0
- package/agent/backup/resultValidation.ts +85 -0
- package/agent/backup/routes.ts +160 -0
- package/agent/backup/runner.ts +91 -0
- package/agent/backup/standaloneWorker.ts +23 -0
- package/agent/backup/workerExecutor.ts +40 -0
- package/agent/backup/workerFactory.ts +21 -0
- package/agent/backup/workerProtocol.ts +10 -0
- package/agent/db/core.ts +141 -0
- package/agent/db/csvExport.ts +172 -0
- package/agent/db/exportTimeout.ts +190 -0
- package/agent/db/query.ts +82 -0
- package/agent/db/queryTimeout.ts +76 -0
- package/agent/db/schema.ts +79 -0
- package/agent/db/tables.ts +157 -0
- package/agent/db/types.ts +118 -0
- package/agent/db/workerFactory.ts +25 -0
- package/agent/db.ts +43 -1099
- package/agent/errorMessage.ts +9 -0
- package/agent/flow/routes.ts +250 -0
- package/agent/flow/service.ts +266 -0
- package/agent/flow/types.ts +34 -0
- package/agent/flow/validation.ts +230 -0
- package/agent/index.ts +5 -17
- package/agent/logger.ts +121 -17
- package/agent/managedTarget.ts +43 -0
- package/agent/manager/config.ts +84 -0
- package/agent/manager/logs.ts +83 -0
- package/agent/manager/process.ts +233 -0
- package/agent/manager/storage.ts +28 -0
- package/agent/manager/types.ts +38 -0
- package/agent/manager.ts +10 -490
- package/agent/queue/routes.ts +128 -0
- package/agent/queue/runtime.ts +119 -0
- package/agent/queue/types.ts +34 -0
- package/agent/queue/validation.ts +100 -0
- package/agent/server/controlRoutes.ts +153 -0
- package/agent/server/databaseRoutes.ts +165 -0
- package/agent/server/errors.ts +48 -0
- package/agent/server/handler.ts +146 -0
- package/agent/server/jsonBody.ts +109 -0
- package/agent/server/lifecycle.ts +72 -0
- package/agent/server/managedRuntime.ts +41 -0
- package/agent/server/policy.ts +60 -0
- package/agent/server/router.ts +150 -0
- package/agent/server/types.ts +39 -0
- package/agent/server/workflowReadRoutes.ts +80 -0
- package/agent/server.ts +16 -441
- package/agent/shutdown.ts +156 -0
- package/agent/workflow/codec.ts +78 -0
- package/agent/workflow/detail.ts +63 -0
- package/agent/workflow/list.ts +87 -0
- package/agent/workflow/runtime.ts +274 -0
- package/agent/workflow/runtimeConfig.ts +103 -0
- package/agent/workflow/runtimeRoutes.ts +162 -0
- package/agent/workflow/sqlite.ts +52 -0
- package/agent/workflow/stats.ts +79 -0
- package/agent/workflow/types.ts +81 -0
- package/agent/workflow/validation.ts +114 -0
- package/agent/workflows.ts +14 -489
- package/dist/.vite/manifest.json +595 -234
- package/dist/assets/AddJob-UUa2fSXE.js +3 -0
- package/dist/assets/Alerts-7oGYfQMb.js +1 -0
- package/dist/assets/{AreaChart-REK4ZsTC.js → AreaChart-w0ruozXH.js} +1 -1
- package/dist/assets/Benchmark-C2NFp_kj.js +1 -0
- package/dist/assets/BulkAddJobs-CL4yBae-.js +5 -0
- package/dist/assets/Button-BPqpRZJD.js +1 -0
- package/dist/assets/Card-C7NMhqUt.js +1 -0
- package/dist/assets/{CopilotPanel-COLw-atS.js → CopilotPanel-BQ2Cgo9p.js} +3 -3
- package/dist/assets/Cron-DfWBhoBf.js +1 -0
- package/dist/assets/CronManager-DAK7RSOX.js +1 -0
- package/dist/assets/Database-DqL7giEj.js +3 -0
- package/dist/assets/Diagnostics-D98YwN-6.js +1 -0
- package/dist/assets/Dlq-BHmA5PTI.js +1 -0
- package/dist/assets/DlqControl-DihxvP-i.js +1 -0
- package/dist/assets/DlqPro-ncgnDtpE.js +1 -0
- package/dist/assets/Flows-B5N5Cil0.js +2 -0
- package/dist/assets/JobInspector-B40nFiGh.js +2 -0
- package/dist/assets/Jobs-Bla3IEef.js +1 -0
- package/dist/assets/JobsPro-CnczVx7x.js +1 -0
- package/dist/assets/Logs-T91Q3cH0.js +1 -0
- package/dist/assets/LogsPro-CMn6-Wwv.js +1 -0
- package/dist/assets/{McpServer-CbaU52l2.js → McpServer-BQBabwW-.js} +2 -2
- package/dist/assets/Metrics-DxT_RI2B.js +1 -0
- package/dist/assets/MetricsPro-Cjen-nAP.js +1 -0
- package/dist/assets/Overview-XN2ej8ph.js +1 -0
- package/dist/assets/PageHeader-BXlZSBcc.js +1 -0
- package/dist/assets/Pagination-Q-vf4hsX.js +1 -0
- package/dist/assets/QueueActions-C-Aferd6.js +1 -0
- package/dist/assets/QueueControl-B8Q9NX6t.js +1 -0
- package/dist/assets/QueueDetail-BMhG0jUB.js +1 -0
- package/dist/assets/QueueDetailPro-BKTTyLR4.js +1 -0
- package/dist/assets/Queues-COqPMoY9.js +1 -0
- package/dist/assets/QueuesOverview-pWdrPy8Z.js +1 -0
- package/dist/assets/S3Backup-BmsZ8xtq.js +1 -0
- package/dist/assets/S3BackupPro-AtlsPUJL.js +3 -0
- package/dist/assets/ServerControl-D3XwHr72.js +3 -0
- package/dist/assets/Settings-BofKZoHn.js +1 -0
- package/dist/assets/StatusBadge-B0I7hNBX.js +1 -0
- package/dist/assets/Usage-DAR9E22v.js +1 -0
- package/dist/assets/UsagePro-Bc30N2Lo.js +1 -0
- package/dist/assets/Webhooks-Bg2k1PU4.js +1 -0
- package/dist/assets/Workers-BqUZMJVQ.js +1 -0
- package/dist/assets/WorkersPro-C9aSadUh.js +1 -0
- package/dist/assets/Workflows-BEFMfHmo.js +1 -0
- package/dist/assets/api-CQ5Z7xR7.js +1 -0
- package/dist/assets/bq-BeBxa743.js +1 -0
- package/dist/assets/cn-zFEt5SO8.js +1 -0
- package/dist/assets/connectionStore-DbBYqHEY.js +1 -0
- package/dist/assets/controlConnectionEpoch-CFSO6237.js +1 -0
- package/dist/assets/feedback-BnGE2oW2.js +1 -0
- package/dist/assets/{form-DsUq0w8s.js → form-tC20r4Ma.js} +1 -1
- package/dist/assets/format-Cg7VCIZW.js +1 -0
- package/dist/assets/icons-CIvFfU2b.js +1 -0
- package/dist/assets/index-Cui9IwWy.js +8 -0
- package/dist/assets/index-DOze4TKg.css +1 -0
- package/dist/assets/install-Cd9Is10G.js +3 -0
- package/dist/assets/{react-vendor-Bjui4wS4.js → react-vendor-CyjgmGTq.js} +3 -3
- package/dist/assets/responses-kPO4rSu5.js +1 -0
- package/dist/assets/toastStore-CS_9Xndn.js +1 -0
- package/dist/assets/usePolledData-DzlUpBOO.js +1 -0
- package/dist/assets/{useServerActionGuard-CItj3hvh.js → useServerActionGuard-BlH-Kso1.js} +1 -1
- package/dist/assets/useThroughputSeries-C2pa1Wd_.js +1 -0
- package/dist/index.html +16 -5
- package/package.json +12 -3
- package/scripts/embedded.gen.ts +152 -126
- package/scripts/serve.ts +96 -484
- package/scripts/serveHandler.ts +217 -0
- package/scripts/servePolicy.ts +133 -0
- package/dist/assets/AddJob-CG28MSxU.js +0 -3
- package/dist/assets/Alerts-D9JojJrj.js +0 -1
- package/dist/assets/Benchmark-BVjUH9dG.js +0 -1
- package/dist/assets/BulkAddJobs-BHzUCMj_.js +0 -5
- package/dist/assets/Cron-DoEHDHwy.js +0 -1
- package/dist/assets/CronManager-vulcesqJ.js +0 -1
- package/dist/assets/Database-DlB_t04Z.js +0 -3
- package/dist/assets/Diagnostics-DddCUgeR.js +0 -1
- package/dist/assets/Dlq-DMKrsSMP.js +0 -1
- package/dist/assets/DlqControl-9m0Re_TH.js +0 -1
- package/dist/assets/DlqPro-Dmw-0wta.js +0 -1
- package/dist/assets/Flows-C3-9nXW4.js +0 -1
- package/dist/assets/JobInspector-DwIfABmI.js +0 -2
- package/dist/assets/Jobs-j8tTS3NC.js +0 -1
- package/dist/assets/JobsPro-CS__CJJl.js +0 -1
- package/dist/assets/Logs-BB-VDPA1.js +0 -1
- package/dist/assets/LogsPro-RNtSQZRT.js +0 -1
- package/dist/assets/Metrics-ivcddPwT.js +0 -1
- package/dist/assets/MetricsPro-xjZno4vg.js +0 -1
- package/dist/assets/Overview-D84aD2ph.js +0 -1
- package/dist/assets/Pagination-Bfi4vkpp.js +0 -1
- package/dist/assets/QueueActions-DTbD6uek.js +0 -1
- package/dist/assets/QueueControl-B5xK_SQ-.js +0 -1
- package/dist/assets/QueueDetail-BH59kwm9.js +0 -1
- package/dist/assets/QueueDetailPro-DoN25ndy.js +0 -1
- package/dist/assets/Queues-CKMJqFHX.js +0 -1
- package/dist/assets/QueuesOverview-BX1lzojl.js +0 -1
- package/dist/assets/S3Backup-Bz3GfoZ9.js +0 -1
- package/dist/assets/S3BackupPro-BC_6-uX-.js +0 -2
- package/dist/assets/ServerControl-CSSrN8G5.js +0 -3
- package/dist/assets/Settings-DkqmZUIS.js +0 -1
- package/dist/assets/Usage-Ba20p0xd.js +0 -1
- package/dist/assets/UsagePro-C2pQwjN9.js +0 -1
- package/dist/assets/Webhooks-CVuuvb0L.js +0 -1
- package/dist/assets/Workers-C1wPU6CS.js +0 -1
- package/dist/assets/WorkersPro-B0vEkC0T.js +0 -1
- package/dist/assets/Workflows-C8Qmkflf.js +0 -1
- package/dist/assets/cn-CMFYpumL.js +0 -1
- package/dist/assets/connectionStore-Cd9-3Fw0.js +0 -1
- package/dist/assets/index-BCNzbhhD.css +0 -1
- package/dist/assets/index-CAtsVmyh.js +0 -8
- package/dist/assets/install-D0eRLf9F.js +0 -3
- package/dist/assets/useThroughputSeries-DjxGN8ob.js +0 -1
package/scripts/serve.ts
CHANGED
|
@@ -1,513 +1,139 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Standalone
|
|
4
|
-
*
|
|
5
|
-
* One binary, three jobs:
|
|
6
|
-
* 1. Serves the dashboard SPA from assets embedded at compile time.
|
|
7
|
-
* 2. Proxies /api/* to the bunqueue server (BUNQUEUE_URL, default :6790) —
|
|
8
|
-
* same-origin like the dev proxy, so no CORS and SSE streams through.
|
|
9
|
-
* 3. Runs the control agent (start/stop/restart bunqueue) on 127.0.0.1:6800
|
|
10
|
-
* with the same Origin-allowlist security as agent/index.ts — and ALSO
|
|
11
|
-
* exposes it same-origin at /agent/* (the served index.html gets
|
|
12
|
-
* `window.__BUNQUEUE_AGENT_URL__ = '/agent'` injected), so the prebuilt
|
|
13
|
-
* SPA finds the agent regardless of AGENT_PORT and even from a remote
|
|
14
|
-
* browser, where loopback :6800 would be unreachable.
|
|
15
|
-
*
|
|
16
|
-
* Control-plane exposure rule (the agent can spawn processes):
|
|
17
|
-
* - Truly local loopback access (the default): /agent/* keeps zero-config
|
|
18
|
-
* reads, and the direct loopback :6800 listener remains local-only.
|
|
19
|
-
* - Non-loopback bind (BIND_ADDR=0.0.0.0): AGENT_TOKEN is mandatory and gates
|
|
20
|
-
* EVERY agent route, including status/log/database reads. The public
|
|
21
|
-
* listener also keeps a Host allowlist; put every public/LAN hostname or IP
|
|
22
|
-
* in AGENT_ALLOWED_HOSTS (or AGENT_ALLOWED_ORIGINS) to admit it. There is no
|
|
23
|
-
* unauthenticated remote-control opt-in.
|
|
24
|
-
* - A loopback bind reached through a reverse proxy follows the same remote
|
|
25
|
-
* rule. TRUST_PROXY, non-loopback allowlists/origins, forwarding headers or
|
|
26
|
-
* a non-loopback request Host all switch the bridge to all-route token auth.
|
|
27
|
-
* - The same signals gate the administrative /api proxy independently with
|
|
28
|
-
* BUNQUEUE_TOKEN; without it, remote/proxied /api access is disabled.
|
|
29
|
-
*
|
|
30
|
-
* Env: PORT (dashboard, default 8080) · BIND_ADDR (default 127.0.0.1) ·
|
|
31
|
-
* BUNQUEUE_URL · AGENT_PORT · AGENT_ALLOWED_ORIGINS · AGENT_ALLOWED_HOSTS ·
|
|
32
|
-
* AGENT_TOKEN · BUNQUEUE_TOKEN · TRUST_PROXY · BUNQUEUE_START_CMD ·
|
|
33
|
-
* HTTP_PORT · TCP_PORT · BUNQUEUE_DATA_PATH · LOG_LEVEL (pino level, info)
|
|
3
|
+
* Standalone dashboard: serves embedded assets, proxies /api, and exposes the
|
|
4
|
+
* local control agent through a same-origin /agent bridge.
|
|
34
5
|
*/
|
|
35
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
36
6
|
import { logger } from '../agent/logger';
|
|
7
|
+
import { BunqueueBackupRunner } from '../agent/backup/runner';
|
|
8
|
+
import { setBackupWorkerUrl } from '../agent/backup/workerFactory';
|
|
37
9
|
import { setQueryWorkerUrl } from '../agent/db';
|
|
10
|
+
import { QueueOperationsRuntime } from '../agent/queue/runtime';
|
|
11
|
+
import { AgentLifecycleGate, createFetchHandler, resolveAllowedOrigins } from '../agent/server';
|
|
12
|
+
import { installAgentShutdown } from '../agent/shutdown';
|
|
13
|
+
import { WorkflowRuntime } from '../agent/workflow/runtime';
|
|
14
|
+
import { createServeHandler } from './serveHandler';
|
|
38
15
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
headers,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** True loopback names/addresses. 0.0.0.0 is a wildcard, never local-only. */
|
|
65
|
-
export function isLoopbackHost(host: string): boolean {
|
|
66
|
-
const value = hostnameOf(host);
|
|
67
|
-
const octets = value.split('.');
|
|
68
|
-
const ipv4Loopback =
|
|
69
|
-
octets.length === 4 &&
|
|
70
|
-
octets[0] === '127' &&
|
|
71
|
-
octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
|
|
72
|
-
return (
|
|
73
|
-
value === 'localhost' ||
|
|
74
|
-
value === '::1' ||
|
|
75
|
-
value === '0:0:0:0:0:0:0:1' ||
|
|
76
|
-
ipv4Loopback
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Bind addresses that mean "only this machine can reach the listener". */
|
|
81
|
-
export function isLoopbackBind(host: string): boolean {
|
|
82
|
-
return isLoopbackHost(host);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function configuredValues(value: string | undefined): string[] {
|
|
86
|
-
return (value ?? '')
|
|
87
|
-
.split(',')
|
|
88
|
-
.map((entry) => entry.trim())
|
|
89
|
-
.filter(Boolean);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* A loopback socket can still be public through a reverse proxy. These are
|
|
94
|
-
* operator-controlled signals that the same-origin /agent bridge is intended
|
|
95
|
-
* to be reachable beyond the machine, so its reads and writes must both use
|
|
96
|
-
* the bearer-token policy.
|
|
97
|
-
*/
|
|
98
|
-
export function remoteBridgeRequiresToken(
|
|
99
|
-
loopbackBind: boolean,
|
|
100
|
-
env: Record<string, string | undefined> = process.env
|
|
101
|
-
): boolean {
|
|
102
|
-
if (!loopbackBind || env.TRUST_PROXY === '1') return true;
|
|
103
|
-
const configured = [
|
|
104
|
-
...configuredValues(env.AGENT_ALLOWED_HOSTS),
|
|
105
|
-
...configuredValues(env.AGENT_ALLOWED_ORIGINS),
|
|
106
|
-
];
|
|
107
|
-
return configured.some((value) => !isLoopbackHost(value));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const PROXY_HINT_HEADERS = [
|
|
111
|
-
'forwarded',
|
|
112
|
-
'x-forwarded-for',
|
|
113
|
-
'x-forwarded-host',
|
|
114
|
-
'x-forwarded-proto',
|
|
115
|
-
] as const;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Per-request backstop for a public Host/Origin or a proxy that adds forwarding
|
|
119
|
-
* metadata. Forwarded-header presence is only used to demand stronger auth;
|
|
120
|
-
* values remain untrusted unless TRUST_PROXY enables the Origin comparison.
|
|
121
|
-
*/
|
|
122
|
-
export function isRemoteBridgeRequest(req: Request, remotePolicy = false): boolean {
|
|
123
|
-
if (remotePolicy || PROXY_HINT_HEADERS.some((name) => req.headers.has(name))) return true;
|
|
124
|
-
const origin = req.headers.get('origin');
|
|
125
|
-
if (origin && !isLoopbackHost(origin)) return true;
|
|
126
|
-
const host = req.headers.get('host') ?? new URL(req.url).hostname;
|
|
127
|
-
return !isLoopbackHost(host);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Exact, timing-safe bearer comparison for the standalone admin-API proxy. */
|
|
131
|
-
export function apiTokenOk(req: Request, token: string | undefined): boolean {
|
|
132
|
-
if (!token) return false;
|
|
133
|
-
const authorization = req.headers.get('authorization');
|
|
134
|
-
if (!authorization?.startsWith('Bearer ')) return false;
|
|
135
|
-
const supplied = Buffer.from(authorization.slice('Bearer '.length));
|
|
136
|
-
const expected = Buffer.from(token);
|
|
137
|
-
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Host policy shared by main() and deployment regression tests. */
|
|
141
|
-
export function resolveServeAllowedHosts(
|
|
142
|
-
bindHost: string,
|
|
143
|
-
allowedOrigins: string[],
|
|
144
|
-
env: Record<string, string | undefined> = process.env
|
|
145
|
-
): string[] {
|
|
146
|
-
const originHosts = allowedOrigins
|
|
147
|
-
.map((origin) => {
|
|
148
|
-
try {
|
|
149
|
-
return new URL(origin).hostname;
|
|
150
|
-
} catch {
|
|
151
|
-
return '';
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
.filter(Boolean);
|
|
155
|
-
const concreteBindHosts = bindHost === '0.0.0.0' || bindHost === '::' ? [] : [bindHost];
|
|
156
|
-
return resolveAllowedHosts(env, [...originHosts, ...concreteBindHosts]);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* May the process-spawning bridge be enabled? Truly local access stays
|
|
161
|
-
* zero-config; any remote deployment policy requires a real token. This
|
|
162
|
-
* intentionally ignores the former unauthenticated escape hatch.
|
|
163
|
-
*/
|
|
164
|
-
export function remoteControlEnabled(
|
|
165
|
-
remotePolicy: boolean,
|
|
166
|
-
env: Record<string, string | undefined> = process.env
|
|
167
|
-
): boolean {
|
|
168
|
-
if (!remotePolicy) return true;
|
|
169
|
-
return Boolean(env.AGENT_TOKEN?.trim());
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Build the inner agent URL from the bridged path. `new URL(rest, base)` throws
|
|
174
|
-
* on a path like `//x:y/z` (it reads `//` as an authority); assigning .pathname
|
|
175
|
-
* never reinterprets it, so a hostile path can't crash the handler.
|
|
176
|
-
*/
|
|
177
|
-
export function agentSubUrl(pathname: string, search: string): URL {
|
|
178
|
-
const rest = pathname.slice('/agent'.length) || '/';
|
|
179
|
-
const sub = new URL('http://agent.internal');
|
|
180
|
-
sub.pathname = rest.startsWith('/') ? rest : `/${rest}`;
|
|
181
|
-
sub.search = search;
|
|
182
|
-
return sub;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export interface ServeHandlerOptions {
|
|
186
|
-
/** bunqueue base URL the /api prefix proxies to (no trailing slash). */
|
|
187
|
-
api: string;
|
|
188
|
-
/** index.html with the agent base already injected. */
|
|
189
|
-
indexHtml: string;
|
|
190
|
-
/** embedded dist assets: request path → on-disk path. */
|
|
191
|
-
assets: Record<string, string>;
|
|
192
|
-
/** Local policy: zero-config reads, configured token on mutations. */
|
|
193
|
-
agentHandle: (req: Request) => Response | Promise<Response>;
|
|
194
|
-
/** Remote/proxied policy: configured token on every agent route. */
|
|
195
|
-
remoteAgentHandle: (req: Request) => Response | Promise<Response>;
|
|
196
|
-
allowedOrigins: string[];
|
|
197
|
-
allowedHosts?: string[];
|
|
198
|
-
/** false → /agent/* is refused under a remote policy without AGENT_TOKEN. */
|
|
199
|
-
agentBridge: boolean;
|
|
200
|
-
/** Whether the remote bridge has a non-empty AGENT_TOKEN to enforce. */
|
|
201
|
-
agentTokenConfigured: boolean;
|
|
202
|
-
/**
|
|
203
|
-
* Bearer required by /api on every remote/proxied request. It is deliberately
|
|
204
|
-
* independent of AGENT_TOKEN because the two credentials protect different
|
|
205
|
-
* principals and can be rotated independently.
|
|
206
|
-
*/
|
|
207
|
-
apiToken?: string;
|
|
208
|
-
/** Deployment-level signal that even a loopback bind is externally proxied. */
|
|
209
|
-
remoteBridgePolicy?: boolean;
|
|
210
|
-
/**
|
|
211
|
-
* TRUST_PROXY=1 — a reverse proxy in front of us owns X-Forwarded-Host, so it
|
|
212
|
-
* may be believed. Off by default: the header is otherwise client-settable
|
|
213
|
-
* and would let any direct caller declare itself same-origin.
|
|
214
|
-
*/
|
|
215
|
-
trustProxy?: boolean;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* The dashboard listener: static assets + /api proxy + same-origin /agent
|
|
220
|
-
* bridge. Factored out of the Bun.serve call so the policy is unit-testable.
|
|
221
|
-
*/
|
|
222
|
-
export function createServeHandler(opts: ServeHandlerOptions) {
|
|
223
|
-
const {
|
|
224
|
-
api,
|
|
225
|
-
indexHtml,
|
|
226
|
-
assets,
|
|
227
|
-
agentHandle,
|
|
228
|
-
remoteAgentHandle,
|
|
229
|
-
allowedOrigins,
|
|
230
|
-
allowedHosts,
|
|
231
|
-
agentBridge,
|
|
232
|
-
agentTokenConfigured,
|
|
233
|
-
apiToken,
|
|
234
|
-
remoteBridgePolicy = false,
|
|
235
|
-
trustProxy = false,
|
|
236
|
-
} = opts;
|
|
237
|
-
const secure = withSecurityHeaders;
|
|
238
|
-
const indexResponse = () =>
|
|
239
|
-
new Response(indexHtml, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
240
|
-
|
|
241
|
-
return async function handle(req: Request): Promise<Response> {
|
|
242
|
-
const url = new URL(req.url);
|
|
243
|
-
|
|
244
|
-
// DNS-rebinding defense across every route (/api proxy, /agent, assets):
|
|
245
|
-
// a rebound page sends its own domain as Host. Main configures this on both
|
|
246
|
-
// loopback and network binds; tests/custom embedders may omit the policy.
|
|
247
|
-
if (!isHostAllowed(req.headers.get('host'), allowedHosts)) {
|
|
248
|
-
return secure(new Response('Host not allowed', { status: 403 }));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// A request whose Origin is the origin this listener serves from is by
|
|
252
|
-
// definition not the cross-site case the allowlist exists for. It is what a
|
|
253
|
-
// LAN browser sends (http://<lan-ip>:PORT), which no static allowlist can
|
|
254
|
-
// predict — treat it as allowed everywhere below.
|
|
255
|
-
//
|
|
256
|
-
// Compared by HOST, not by full origin: this listener only ever speaks
|
|
257
|
-
// plain http, so behind a TLS-terminating reverse proxy the browser sends
|
|
258
|
-
// `https://dash.example.com` while req.url reads `http://dash.example.com`.
|
|
259
|
-
// A full-origin compare would 403 every mutation on a proxied deployment
|
|
260
|
-
// while read-only GETs (which carry no Origin) kept working — a dashboard
|
|
261
|
-
// that looks healthy and fails on click. The host is the part that
|
|
262
|
-
// identifies a cross-site page (its Origin carries its OWN host), so it is
|
|
263
|
-
// the part worth comparing. x-forwarded-host is honoured for proxies that
|
|
264
|
-
// rewrite Host.
|
|
265
|
-
const origin = req.headers.get('origin');
|
|
266
|
-
let originHost = '';
|
|
267
|
-
try {
|
|
268
|
-
originHost = origin ? new URL(origin).host.toLowerCase() : '';
|
|
269
|
-
} catch {
|
|
270
|
-
originHost = ''; // unparseable Origin — treat as cross-site
|
|
271
|
-
}
|
|
272
|
-
// X-Forwarded-Host is attacker-controlled unless something in front of us
|
|
273
|
-
// is guaranteed to overwrite it: a direct caller can send
|
|
274
|
-
// `Origin: https://evil.example` + `x-forwarded-host: evil.example` and
|
|
275
|
-
// declare ITSELF same-origin. So it is consulted only when the operator
|
|
276
|
-
// sets TRUST_PROXY=1, which asserts a reverse proxy owns that header.
|
|
277
|
-
// Without it, a proxy that preserves Host still works (the Host compare
|
|
278
|
-
// below covers it); only a Host-REWRITING proxy needs the opt-in.
|
|
279
|
-
const fwd = trustProxy ? req.headers.get('x-forwarded-host')?.split(',') : undefined;
|
|
280
|
-
const forwardedHost = fwd?.[fwd.length - 1]?.trim().toLowerCase();
|
|
281
|
-
const sameOrigin =
|
|
282
|
-
originHost !== '' &&
|
|
283
|
-
(originHost === url.host.toLowerCase() || originHost === forwardedHost);
|
|
284
|
-
|
|
285
|
-
// Same-origin bridge to the control agent: strip the /agent prefix and
|
|
286
|
-
// hand the request to the in-process agent handler (no loopback hop).
|
|
287
|
-
// The selected agent handler still applies Host, Origin and token policy;
|
|
288
|
-
// this bridge changes reachability, never authorization.
|
|
289
|
-
if (url.pathname === '/agent' || url.pathname.startsWith('/agent/')) {
|
|
290
|
-
const remoteRequest = isRemoteBridgeRequest(req, remoteBridgePolicy);
|
|
291
|
-
if (!agentBridge || (remoteRequest && !agentTokenConfigured)) {
|
|
292
|
-
return secure(
|
|
293
|
-
Response.json(
|
|
294
|
-
{
|
|
295
|
-
ok: false,
|
|
296
|
-
error:
|
|
297
|
-
'Control agent disabled for remote or proxied access. Set AGENT_TOKEN to expose it.',
|
|
298
|
-
},
|
|
299
|
-
{ status: 403 }
|
|
300
|
-
)
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
const sub = agentSubUrl(url.pathname, url.search);
|
|
304
|
-
const headers = new Headers(req.headers);
|
|
305
|
-
// Drop a same-origin Origin the agent's static allowlist cannot know
|
|
306
|
-
// (a LAN IP / hostname alias). Loopback origins are already allowlisted,
|
|
307
|
-
// so they are forwarded untouched and CORS behaviour is unchanged.
|
|
308
|
-
if (sameOrigin && origin && !isOriginAllowed(origin, allowedOrigins)) headers.delete('origin');
|
|
309
|
-
const handleAgent = remoteRequest ? remoteAgentHandle : agentHandle;
|
|
310
|
-
return secure(
|
|
311
|
-
await handleAgent(
|
|
312
|
-
new Request(sub.href, {
|
|
313
|
-
method: req.method,
|
|
314
|
-
headers,
|
|
315
|
-
body: req.body,
|
|
316
|
-
signal: req.signal,
|
|
317
|
-
})
|
|
318
|
-
)
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Same-origin proxy to the bunqueue server (mirrors the Vite dev proxy).
|
|
323
|
-
if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
|
|
324
|
-
// The proxy talks to bunqueue's admin API, so it needs the same drive-by
|
|
325
|
-
// CSRF gate as the agent: a cross-site page must not reach it.
|
|
326
|
-
if (!sameOrigin && !isOriginAllowed(origin, allowedOrigins)) {
|
|
327
|
-
return secure(
|
|
328
|
-
Response.json({ ok: false, error: 'Origin not allowed' }, { status: 403 })
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
// Host/Origin checks stop browser CSRF and DNS rebinding; they do not
|
|
332
|
-
// authenticate curl, LAN peers or callers whose upstream has AUTH_TOKENS
|
|
333
|
-
// disabled. Fail closed for every public/proxied request at this boundary.
|
|
334
|
-
if (isRemoteBridgeRequest(req, remoteBridgePolicy)) {
|
|
335
|
-
if (!apiToken) {
|
|
336
|
-
return secure(
|
|
337
|
-
Response.json(
|
|
338
|
-
{
|
|
339
|
-
ok: false,
|
|
340
|
-
error:
|
|
341
|
-
'Admin API proxy disabled for remote or proxied access. Set BUNQUEUE_TOKEN to expose it.',
|
|
342
|
-
},
|
|
343
|
-
{ status: 403 }
|
|
344
|
-
)
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
if (!apiTokenOk(req, apiToken)) {
|
|
348
|
-
return secure(
|
|
349
|
-
Response.json(
|
|
350
|
-
{ ok: false, error: 'A valid BUNQUEUE_TOKEN bearer token is required.' },
|
|
351
|
-
{ status: 401, headers: { 'WWW-Authenticate': 'Bearer' } }
|
|
352
|
-
)
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
const target = api + (url.pathname.slice(4) || '/') + url.search;
|
|
357
|
-
let res: Response;
|
|
358
|
-
try {
|
|
359
|
-
res = await fetch(target, {
|
|
360
|
-
method: req.method,
|
|
361
|
-
headers: req.headers,
|
|
362
|
-
body: req.body,
|
|
363
|
-
redirect: 'manual',
|
|
364
|
-
signal: req.signal,
|
|
365
|
-
});
|
|
366
|
-
} catch (err) {
|
|
367
|
-
// bunqueue not running / wrong BUNQUEUE_URL: answer in the shape the
|
|
368
|
-
// dashboard parses instead of letting Bun render an HTML error page.
|
|
369
|
-
return secure(
|
|
370
|
-
Response.json(
|
|
371
|
-
{ ok: false, error: `bunqueue unreachable at ${api}: ${(err as Error).message}` },
|
|
372
|
-
{ status: 502 }
|
|
373
|
-
)
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
// Bun's fetch advertises accept-encoding upstream and transparently
|
|
377
|
-
// DECOMPRESSES the body, but leaves the upstream headers intact. If the
|
|
378
|
-
// bunqueue server sits behind any gzip-compressing proxy, forwarding
|
|
379
|
-
// those headers labels plaintext as gzip and the browser fails to decode
|
|
380
|
-
// every /api response (ERR_CONTENT_DECODING_FAILED) — strip them.
|
|
381
|
-
const headers = new Headers(res.headers);
|
|
382
|
-
headers.delete('content-encoding');
|
|
383
|
-
headers.delete('content-length');
|
|
384
|
-
headers.delete('transfer-encoding');
|
|
385
|
-
return secure(
|
|
386
|
-
new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Embedded static assets with SPA history fallback. A missing fingerprinted
|
|
391
|
-
// /assets/* file must 404 (as docker/Caddyfile does) — falling back to
|
|
392
|
-
// index.html would feed HTML to a stale chunk import() and mask the miss.
|
|
393
|
-
// index.html (direct or via fallback) is served from the injected copy.
|
|
394
|
-
const key = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
395
|
-
if (key === '/index.html') return secure(indexResponse());
|
|
396
|
-
const asset = assets[key];
|
|
397
|
-
if (asset) return secure(new Response(Bun.file(asset)));
|
|
398
|
-
if (url.pathname.startsWith('/assets/')) {
|
|
399
|
-
return secure(new Response('Not found', { status: 404 }));
|
|
400
|
-
}
|
|
401
|
-
return secure(indexResponse());
|
|
402
|
-
};
|
|
403
|
-
}
|
|
16
|
+
isLoopbackBind,
|
|
17
|
+
remoteBridgeRequiresToken,
|
|
18
|
+
remoteControlEnabled,
|
|
19
|
+
resolveServeAllowedHosts,
|
|
20
|
+
withSecurityHeaders,
|
|
21
|
+
} from './servePolicy';
|
|
22
|
+
|
|
23
|
+
export { createServeHandler } from './serveHandler';
|
|
24
|
+
export {
|
|
25
|
+
agentSubUrl,
|
|
26
|
+
apiTokenOk,
|
|
27
|
+
isLoopbackBind,
|
|
28
|
+
isLoopbackHost,
|
|
29
|
+
isRemoteBridgeRequest,
|
|
30
|
+
RESPONSE_SECURITY_HEADERS,
|
|
31
|
+
remoteBridgeRequiresToken,
|
|
32
|
+
remoteControlEnabled,
|
|
33
|
+
resolveServeAllowedHosts,
|
|
34
|
+
type ServeHandlerOptions,
|
|
35
|
+
withSecurityHeaders,
|
|
36
|
+
} from './servePolicy';
|
|
404
37
|
|
|
405
38
|
async function main(): Promise<void> {
|
|
406
|
-
// Bun
|
|
407
|
-
// Point db.ts at that embedded path in a compiled executable; when this npm bin
|
|
408
|
-
// runs from source, use the real TypeScript module beside agent/db.ts.
|
|
39
|
+
// Compiled Bun entrypoints load the query worker from the embedded bundle.
|
|
409
40
|
const compiled = import.meta.url.includes('/$bunfs/');
|
|
410
41
|
setQueryWorkerUrl(
|
|
411
42
|
compiled
|
|
412
43
|
? new URL('/$bunfs/root/agent/dbQueryWorker.js', 'file:///').href
|
|
413
44
|
: new URL('../agent/dbQueryWorker.ts', import.meta.url).href
|
|
414
45
|
);
|
|
46
|
+
setBackupWorkerUrl(
|
|
47
|
+
compiled
|
|
48
|
+
? new URL('/$bunfs/root/agent/backup/standaloneWorker.js', 'file:///').href
|
|
49
|
+
: null
|
|
50
|
+
);
|
|
415
51
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const HOST = process.env.BIND_ADDR || '127.0.0.1';
|
|
421
|
-
const API = (process.env.BUNQUEUE_URL || 'http://localhost:6790').replace(/\/$/, '');
|
|
422
|
-
const AGENT_PORT = Number(process.env.AGENT_PORT) || 6800;
|
|
423
|
-
|
|
424
|
-
// Control agent — loopback only, allowlisted CORS; the origins this binary
|
|
425
|
-
// serves the dashboard from are allowed automatically.
|
|
52
|
+
const port = Number(process.env.PORT) || 8080;
|
|
53
|
+
const host = process.env.BIND_ADDR || '127.0.0.1';
|
|
54
|
+
const api = (process.env.BUNQUEUE_URL || 'http://localhost:6790').replace(/\/$/, '');
|
|
55
|
+
const agentPort = Number(process.env.AGENT_PORT) || 6800;
|
|
426
56
|
const allowedOrigins = Array.from(
|
|
427
57
|
new Set([
|
|
428
58
|
...resolveAllowedOrigins(),
|
|
429
|
-
`http://localhost:${
|
|
430
|
-
`http://127.0.0.1:${
|
|
59
|
+
`http://localhost:${port}`,
|
|
60
|
+
`http://127.0.0.1:${port}`,
|
|
431
61
|
])
|
|
432
62
|
);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
// listener AND on the dashboard listener for every bind. For a concrete bind
|
|
436
|
-
// address, admit that address automatically. A wildcard bind cannot reveal
|
|
437
|
-
// which LAN/public address clients use, so the operator must list those names
|
|
438
|
-
// or IPs in AGENT_ALLOWED_HOSTS (origins in AGENT_ALLOWED_ORIGINS also count).
|
|
439
|
-
const loopbackBind = isLoopbackBind(HOST);
|
|
440
|
-
const remoteBridgePolicy = remoteBridgeRequiresToken(loopbackBind, process.env);
|
|
441
|
-
const allowedHosts = resolveServeAllowedHosts(HOST, allowedOrigins, process.env);
|
|
442
|
-
|
|
443
|
-
// Imported here rather than at module scope: this file's exported policy
|
|
444
|
-
// helpers are unit-tested, and a top-level import would pull the whole
|
|
445
|
-
// ProcessManager into every test that touches them — which, besides being
|
|
446
|
-
// wasteful, makes Bun attribute a barely-executed copy of agent/manager.ts to
|
|
447
|
-
// the coverage report and hides that module's real coverage.
|
|
448
|
-
const { ProcessManager } = await import('../agent/manager');
|
|
449
|
-
const mgr = new ProcessManager();
|
|
63
|
+
const remoteBridgePolicy = remoteBridgeRequiresToken(isLoopbackBind(host), process.env);
|
|
64
|
+
const allowedHosts = resolveServeAllowedHosts(host, allowedOrigins, process.env);
|
|
450
65
|
const token = process.env.AGENT_TOKEN?.trim() || undefined;
|
|
451
66
|
const apiToken = process.env.BUNQUEUE_TOKEN?.trim() || undefined;
|
|
452
|
-
// Two handlers, same manager: the loopback :6800 listener keeps the Host gate
|
|
453
|
-
// unconditionally (it is always reached as localhost/127.0.0.1), while the
|
|
454
|
-
// bridged one sees the dashboard client's Host and follows `allowedHosts`.
|
|
455
|
-
const localAgentHandle = createFetchHandler(mgr, {
|
|
456
|
-
allowedOrigins,
|
|
457
|
-
allowedHosts,
|
|
458
|
-
token,
|
|
459
|
-
});
|
|
460
|
-
const remoteAgentHandle = createFetchHandler(mgr, {
|
|
461
|
-
allowedOrigins,
|
|
462
|
-
allowedHosts,
|
|
463
|
-
token,
|
|
464
|
-
requireTokenForAll: true,
|
|
465
|
-
});
|
|
466
|
-
// `error` is a backstop: without it an unexpected throw renders Bun's HTML
|
|
467
|
-
// error page (with a stack trace) to the client instead of a plain 500.
|
|
468
|
-
const onError = () => withSecurityHeaders(new Response('Internal error', { status: 500 }));
|
|
469
|
-
Bun.serve({
|
|
470
|
-
port: AGENT_PORT,
|
|
471
|
-
hostname: '127.0.0.1',
|
|
472
|
-
fetch: localAgentHandle,
|
|
473
|
-
error: onError,
|
|
474
|
-
});
|
|
475
|
-
|
|
476
67
|
const agentBridge = remoteControlEnabled(remoteBridgePolicy, process.env);
|
|
477
68
|
if (!agentBridge) {
|
|
478
69
|
logger.warn(
|
|
479
|
-
{ bind:
|
|
70
|
+
{ bind: host, remoteBridgePolicy },
|
|
480
71
|
'remote or proxied bridge policy without AGENT_TOKEN — /agent is disabled (403)'
|
|
481
72
|
);
|
|
482
73
|
}
|
|
483
74
|
if (remoteBridgePolicy && !apiToken) {
|
|
484
75
|
logger.warn(
|
|
485
|
-
{ apiProxy:
|
|
76
|
+
{ apiProxy: api },
|
|
486
77
|
'remote or proxied bridge policy without BUNQUEUE_TOKEN — /api is disabled (403)'
|
|
487
78
|
);
|
|
488
79
|
} else if (remoteBridgePolicy) {
|
|
489
80
|
logger.info(
|
|
490
|
-
{ apiProxy:
|
|
81
|
+
{ apiProxy: api },
|
|
491
82
|
'remote or proxied /api requires the configured BUNQUEUE_TOKEN bearer'
|
|
492
83
|
);
|
|
493
84
|
}
|
|
494
85
|
|
|
495
|
-
//
|
|
496
|
-
// The SPA's lib/bq.ts reads window.__BUNQUEUE_AGENT_URL__ before its baked-in
|
|
497
|
-
// default, so Server Control works whatever AGENT_PORT is — the /agent/*
|
|
498
|
-
// route below forwards to the in-process agent handler.
|
|
86
|
+
// Complete every awaited startup step before installing handlers/listeners.
|
|
499
87
|
const { ASSETS } = await import('./embedded.gen');
|
|
500
88
|
const indexHtml = (await Bun.file(ASSETS['/index.html']).text()).replace(
|
|
501
89
|
'</head>',
|
|
502
90
|
"<script>window.__BUNQUEUE_AGENT_URL__='/agent'</script></head>"
|
|
503
91
|
);
|
|
504
92
|
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
93
|
+
// Keep ProcessManager out of policy-only test imports.
|
|
94
|
+
const { ProcessManager } = await import('../agent/manager');
|
|
95
|
+
const mgr = new ProcessManager();
|
|
96
|
+
const workflowRuntime = new WorkflowRuntime();
|
|
97
|
+
const backupRunner = new BunqueueBackupRunner();
|
|
98
|
+
const queueRuntime = new QueueOperationsRuntime();
|
|
99
|
+
const lifecycle = new AgentLifecycleGate();
|
|
100
|
+
const localAgentHandle = createFetchHandler(
|
|
101
|
+
mgr,
|
|
102
|
+
{ allowedOrigins, allowedHosts, token },
|
|
103
|
+
workflowRuntime,
|
|
104
|
+
backupRunner,
|
|
105
|
+
queueRuntime,
|
|
106
|
+
lifecycle
|
|
107
|
+
);
|
|
108
|
+
const remoteAgentHandle = createFetchHandler(
|
|
109
|
+
mgr,
|
|
110
|
+
{ allowedOrigins, allowedHosts, token, requireTokenForAll: true },
|
|
111
|
+
workflowRuntime,
|
|
112
|
+
backupRunner,
|
|
113
|
+
queueRuntime,
|
|
114
|
+
lifecycle
|
|
115
|
+
);
|
|
116
|
+
const proxyShutdown = new AbortController();
|
|
117
|
+
const stopAccepting: Array<() => unknown | Promise<unknown>> = [
|
|
118
|
+
() => proxyShutdown.abort(new Error('Dashboard is shutting down')),
|
|
119
|
+
];
|
|
120
|
+
// Register before opening listeners so no startup window uses default signal handling.
|
|
121
|
+
installAgentShutdown(localAgentHandle, { stopAccepting });
|
|
122
|
+
const onError = () => withSecurityHeaders(new Response('Internal error', { status: 500 }));
|
|
123
|
+
|
|
124
|
+
const agentServer = Bun.serve({
|
|
125
|
+
port: agentPort,
|
|
126
|
+
hostname: '127.0.0.1',
|
|
127
|
+
fetch: localAgentHandle,
|
|
128
|
+
error: onError,
|
|
129
|
+
});
|
|
130
|
+
stopAccepting.push(() => agentServer.stop());
|
|
131
|
+
|
|
132
|
+
const dashboardServer = Bun.serve({
|
|
133
|
+
port,
|
|
134
|
+
hostname: host,
|
|
509
135
|
fetch: createServeHandler({
|
|
510
|
-
api
|
|
136
|
+
api,
|
|
511
137
|
indexHtml,
|
|
512
138
|
assets: ASSETS,
|
|
513
139
|
agentHandle: localAgentHandle,
|
|
@@ -517,33 +143,19 @@ async function main(): Promise<void> {
|
|
|
517
143
|
agentBridge,
|
|
518
144
|
agentTokenConfigured: Boolean(token),
|
|
519
145
|
apiToken,
|
|
146
|
+
apiShutdownSignal: proxyShutdown.signal,
|
|
520
147
|
remoteBridgePolicy,
|
|
521
148
|
trustProxy: process.env.TRUST_PROXY === '1',
|
|
522
149
|
}),
|
|
523
150
|
error: onError,
|
|
524
151
|
});
|
|
525
|
-
|
|
526
|
-
// Stop the managed server before exiting — without this, Ctrl-C / SIGTERM on
|
|
527
|
-
// the binary orphans the spawned bunqueue child (it reparents to PID 1 and
|
|
528
|
-
// keeps holding the ports and the SQLite db, so the next start fails).
|
|
529
|
-
let shuttingDown = false;
|
|
530
|
-
const shutdown = (signal: string) => {
|
|
531
|
-
if (shuttingDown) return;
|
|
532
|
-
shuttingDown = true;
|
|
533
|
-
logger.info({ signal }, 'signal received, stopping managed server');
|
|
534
|
-
// shutdown(), not stop(): it latches, so a restart() already in flight
|
|
535
|
-
// can't spawn a fresh child after we've stopped the old one (mirrors
|
|
536
|
-
// agent/index.ts).
|
|
537
|
-
void mgr.shutdown().finally(() => process.exit(0));
|
|
538
|
-
};
|
|
539
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
540
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
152
|
+
stopAccepting.push(() => dashboardServer.stop());
|
|
541
153
|
|
|
542
154
|
logger.info(
|
|
543
155
|
{
|
|
544
|
-
dashboard: `http://${
|
|
545
|
-
apiProxy:
|
|
546
|
-
agent: `http://127.0.0.1:${
|
|
156
|
+
dashboard: `http://${host}:${port}`,
|
|
157
|
+
apiProxy: api,
|
|
158
|
+
agent: `http://127.0.0.1:${agentPort}/control`,
|
|
547
159
|
agentBridge,
|
|
548
160
|
remoteBridgePolicy,
|
|
549
161
|
allowedHosts,
|