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/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', `${UI_AUTH_COOKIE}=${uiAccessToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800`);
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 {