labgate 0.5.14 → 0.5.16
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 +35 -1
- package/dist/lib/container.js +39 -17
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/slurm-cli-passthrough.d.ts +6 -0
- package/dist/lib/slurm-cli-passthrough.js +53 -10
- package/dist/lib/slurm-cli-passthrough.js.map +1 -1
- package/dist/lib/ui.js +73 -1
- package/dist/lib/ui.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/ui.js
CHANGED
|
@@ -56,6 +56,8 @@ const FONTS_DIR = (0, path_1.resolve)(__dirname, '..', '..', 'node_modules', 'ge
|
|
|
56
56
|
const WRITE_TOKEN_PLACEHOLDER = '__LABGATE_WRITE_TOKEN__';
|
|
57
57
|
const UI_WRITE_TOKEN = (0, crypto_1.randomBytes)(24).toString('hex');
|
|
58
58
|
const UI_AUTH_COOKIE = 'labgate_ui_token';
|
|
59
|
+
const UI_SHORT_LINK_PREFIX = '/s/';
|
|
60
|
+
const DASHBOARD_LINK_FILE = '.labgate-dashboard-url';
|
|
59
61
|
const LABGATE_INSTRUCTION_START = '<!-- LABGATE_SESSION_INSTRUCTION_START -->';
|
|
60
62
|
const LABGATE_INSTRUCTION_END = '<!-- LABGATE_SESSION_INSTRUCTION_END -->';
|
|
61
63
|
// ── SLURM module state (initialised in startUI when slurm.enabled) ──
|
|
@@ -130,6 +132,44 @@ function isAuthorizedRequest(req, reqUrl, accessToken) {
|
|
|
130
132
|
return { ok: true, tokenFromQuery: true };
|
|
131
133
|
return { ok: false, tokenFromQuery: false };
|
|
132
134
|
}
|
|
135
|
+
function buildUiAuthCookie(accessToken) {
|
|
136
|
+
return `${UI_AUTH_COOKIE}=${accessToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800`;
|
|
137
|
+
}
|
|
138
|
+
function getDashboardLinkPath() {
|
|
139
|
+
return (0, path_1.join)((0, config_js_1.getSandboxHome)(), DASHBOARD_LINK_FILE);
|
|
140
|
+
}
|
|
141
|
+
function writeDashboardLink(url) {
|
|
142
|
+
const target = getDashboardLinkPath();
|
|
143
|
+
(0, config_js_1.ensurePrivateDir)((0, path_1.dirname)(target));
|
|
144
|
+
const temp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
145
|
+
try {
|
|
146
|
+
(0, fs_1.writeFileSync)(temp, `${url}\n`, { encoding: 'utf-8', mode: config_js_1.PRIVATE_FILE_MODE });
|
|
147
|
+
(0, config_js_1.ensurePrivateFile)(temp);
|
|
148
|
+
(0, fs_1.renameSync)(temp, target);
|
|
149
|
+
(0, config_js_1.ensurePrivateFile)(target);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
try {
|
|
153
|
+
if ((0, fs_1.existsSync)(temp))
|
|
154
|
+
(0, fs_1.unlinkSync)(temp);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Best effort cleanup.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function clearDashboardLink(expectedUrl) {
|
|
162
|
+
const target = getDashboardLinkPath();
|
|
163
|
+
try {
|
|
164
|
+
const existing = (0, fs_1.readFileSync)(target, 'utf-8').trim();
|
|
165
|
+
if (existing !== expectedUrl)
|
|
166
|
+
return;
|
|
167
|
+
(0, fs_1.unlinkSync)(target);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Best effort cleanup.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
133
173
|
function serveFontFile(url, res) {
|
|
134
174
|
// Only allow specific font files from the geist package
|
|
135
175
|
const match = url.match(/^\/fonts\/([\w-]+\.woff2)$/);
|
|
@@ -2755,8 +2795,10 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
2755
2795
|
const requestedPort = tcpPort ?? 0;
|
|
2756
2796
|
const maxPort = requestedPort + 3;
|
|
2757
2797
|
const uiAccessToken = useTcp ? (0, crypto_1.randomBytes)(24).toString('hex') : '';
|
|
2798
|
+
const uiShortCode = useTcp ? (0, crypto_1.randomBytes)(9).toString('base64url') : '';
|
|
2758
2799
|
let listenPort = requestedPort;
|
|
2759
2800
|
let started = false;
|
|
2801
|
+
let dashboardQuickLink = '';
|
|
2760
2802
|
(0, config_js_1.ensurePrivateDir)((0, path_1.dirname)((0, config_js_1.getConfigPath)()));
|
|
2761
2803
|
if (!useTcp) {
|
|
2762
2804
|
(0, config_js_1.ensurePrivateDir)((0, path_1.dirname)(socketPath));
|
|
@@ -2767,6 +2809,24 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
2767
2809
|
const pathname = reqUrl.pathname;
|
|
2768
2810
|
const method = req.method ?? 'GET';
|
|
2769
2811
|
if (useTcp) {
|
|
2812
|
+
const shortLinkMatch = method === 'GET'
|
|
2813
|
+
? pathname.match(new RegExp(`^${UI_SHORT_LINK_PREFIX}([A-Za-z0-9_-]+)$`))
|
|
2814
|
+
: null;
|
|
2815
|
+
if (shortLinkMatch) {
|
|
2816
|
+
if (shortLinkMatch[1] === uiShortCode) {
|
|
2817
|
+
res.writeHead(302, {
|
|
2818
|
+
Location: '/',
|
|
2819
|
+
'Set-Cookie': buildUiAuthCookie(uiAccessToken),
|
|
2820
|
+
'Cache-Control': 'no-store',
|
|
2821
|
+
});
|
|
2822
|
+
res.end();
|
|
2823
|
+
}
|
|
2824
|
+
else {
|
|
2825
|
+
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
2826
|
+
res.end('Unauthorized. Open the latest quick link shown by `labgate ui`.');
|
|
2827
|
+
}
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2770
2830
|
const auth = isAuthorizedRequest(req, reqUrl, uiAccessToken);
|
|
2771
2831
|
if (!auth.ok) {
|
|
2772
2832
|
if (pathname.startsWith('/api/')) {
|
|
@@ -2779,7 +2839,7 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
2779
2839
|
return;
|
|
2780
2840
|
}
|
|
2781
2841
|
if (auth.tokenFromQuery) {
|
|
2782
|
-
res.setHeader('Set-Cookie',
|
|
2842
|
+
res.setHeader('Set-Cookie', buildUiAuthCookie(uiAccessToken));
|
|
2783
2843
|
}
|
|
2784
2844
|
}
|
|
2785
2845
|
if (method === 'OPTIONS') {
|
|
@@ -2953,6 +3013,14 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
2953
3013
|
if (useTcp) {
|
|
2954
3014
|
const actualPort = server.address()?.port ?? listenPort;
|
|
2955
3015
|
log.step(`Settings: http://localhost:${actualPort}/?token=${uiAccessToken}`);
|
|
3016
|
+
dashboardQuickLink = `http://localhost:${actualPort}${UI_SHORT_LINK_PREFIX}${uiShortCode}`;
|
|
3017
|
+
log.step(`Settings quick link: ${dashboardQuickLink}`);
|
|
3018
|
+
try {
|
|
3019
|
+
writeDashboardLink(dashboardQuickLink);
|
|
3020
|
+
}
|
|
3021
|
+
catch {
|
|
3022
|
+
// Best effort: statusline can still fall back to LABGATE_DASHBOARD_URL/default URL.
|
|
3023
|
+
}
|
|
2956
3024
|
}
|
|
2957
3025
|
else {
|
|
2958
3026
|
try {
|
|
@@ -2986,6 +3054,10 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
2986
3054
|
}
|
|
2987
3055
|
startSSEBroadcast();
|
|
2988
3056
|
});
|
|
3057
|
+
server.on('close', () => {
|
|
3058
|
+
if (dashboardQuickLink)
|
|
3059
|
+
clearDashboardLink(dashboardQuickLink);
|
|
3060
|
+
});
|
|
2989
3061
|
server.on('error', (err) => {
|
|
2990
3062
|
if (!useTcp && err.code === 'EADDRINUSE') {
|
|
2991
3063
|
try {
|