@ours.network/fleet 0.14.0 → 0.15.0

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.
@@ -0,0 +1,70 @@
1
+ import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
2
+ import { chmodSync, lstatSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { replaceFileAtomically } from '../atomic-file.js';
5
+ import { FleetError } from '../application/errors.js';
6
+ import { stateRoot } from '../paths.js';
7
+ const DEFAULT = { version: 1, mode: 'pairing' };
8
+ const b64 = /^[A-Za-z0-9_-]{20,128}$/;
9
+ export function passwordAccess(password) {
10
+ if (password.length < 12 || Buffer.byteLength(password) > 1024)
11
+ throw new FleetError('invalid_request', 'control-panel password must be 12–1024 bytes');
12
+ const salt = randomBytes(16).toString('base64url');
13
+ return { version: 1, mode: 'password', password: {
14
+ salt, hash: scryptSync(password, salt, 32).toString('base64url'),
15
+ } };
16
+ }
17
+ export function verifyPassword(config, supplied) {
18
+ if (config.mode !== 'password' || !config.password || Buffer.byteLength(supplied) > 1024)
19
+ return false;
20
+ const actual = scryptSync(supplied, config.password.salt, 32);
21
+ const expected = Buffer.from(config.password.hash, 'base64url');
22
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
23
+ }
24
+ export class WebAccessStore {
25
+ dir;
26
+ path;
27
+ constructor(dir = join(stateRoot(), 'web')) {
28
+ this.dir = dir;
29
+ this.path = join(dir, 'access.json');
30
+ }
31
+ read() {
32
+ try {
33
+ const stat = lstatSync(this.path);
34
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024)
35
+ throw new FleetError('forbidden', 'web access configuration is not a safe regular file');
36
+ chmodSync(this.path, 0o600);
37
+ const value = JSON.parse(readFileSync(this.path, 'utf8'));
38
+ if (value.version !== 1 || !['pairing', 'password', 'none'].includes(value.mode ?? ''))
39
+ throw new FleetError('forbidden', 'web access configuration is invalid');
40
+ if (value.mode === 'password' && (!value.password || !b64.test(value.password.salt)
41
+ || !b64.test(value.password.hash)))
42
+ throw new FleetError('forbidden', 'web password configuration is invalid');
43
+ return value;
44
+ }
45
+ catch (error) {
46
+ if (error.code === 'ENOENT')
47
+ return DEFAULT;
48
+ throw error;
49
+ }
50
+ }
51
+ write(config) {
52
+ mkdirSync(this.dir, { recursive: true, mode: 0o700 });
53
+ chmodSync(this.dir, 0o700);
54
+ replaceFileAtomically(this.path, JSON.stringify(config, null, 2) + '\n', 0o600);
55
+ chmodSync(this.path, 0o600);
56
+ }
57
+ }
58
+ export function validatePublicOrigin(value) {
59
+ let origin;
60
+ try {
61
+ origin = new URL(value);
62
+ }
63
+ catch {
64
+ throw new FleetError('invalid_request', 'public origin must be an absolute http(s) URL');
65
+ }
66
+ if (!['http:', 'https:'].includes(origin.protocol) || origin.username || origin.password
67
+ || origin.pathname !== '/' || origin.search || origin.hash)
68
+ throw new FleetError('invalid_request', 'public origin must contain only scheme, host, and optional port');
69
+ return origin;
70
+ }
@@ -1,6 +1,7 @@
1
1
  import type { FastifyRequest } from 'fastify';
2
2
  import type { WebSocket } from 'ws';
3
3
  import { TrustedDeviceStore, type TrustedDeviceIssue } from './device-store.js';
4
+ import { type WebAccessConfig } from './access.js';
4
5
  export interface BrowserSession {
5
6
  id: string;
6
7
  csrf: string;
@@ -24,6 +25,7 @@ export declare class WebAuth {
24
25
  private _host;
25
26
  private readonly now;
26
27
  private readonly devices;
28
+ private readonly access;
27
29
  private _bootstrapSecret;
28
30
  private bootstrapExpiresAt;
29
31
  private bootstrapUsed;
@@ -32,15 +34,24 @@ export declare class WebAuth {
32
34
  private readonly tickets;
33
35
  private readonly rates;
34
36
  private readonly sockets;
35
- constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore);
37
+ constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig);
36
38
  get bootstrapSecret(): string;
37
39
  get origin(): string;
38
40
  get host(): string;
39
- setBoundary(origin: string, host: string): void;
41
+ get mode(): WebAccessConfig['mode'];
42
+ get secureCookies(): boolean;
43
+ private allowedOrigins;
44
+ private allowedHosts;
45
+ setBoundary(origin: string, host: string, aliases?: {
46
+ origins?: string[];
47
+ hosts?: string[];
48
+ }): void;
40
49
  /** Mint a replacement for an operator-triggered reauthentication ceremony. */
41
50
  mintBootstrap(): string;
42
51
  validateBoundary(request: FastifyRequest, requireOrigin: boolean): void;
43
52
  exchange(request: FastifyRequest): AuthResult;
53
+ login(request: FastifyRequest, password: string): AuthResult;
54
+ anonymous(request: FastifyRequest): BrowserSession;
44
55
  resume(request: FastifyRequest): AuthResult;
45
56
  authenticate(request: FastifyRequest, mutation?: boolean): BrowserSession;
46
57
  logout(request: FastifyRequest): void;
package/dist/web/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomBytes, timingSafeEqual } from 'node:crypto';
2
2
  import { FleetError } from '../application/errors.js';
3
3
  import { TrustedDeviceStore } from './device-store.js';
4
+ import { verifyPassword } from './access.js';
4
5
  const token = (bytes = 32) => randomBytes(bytes).toString('base64url');
5
6
  const same = (a, b) => {
6
7
  const left = Buffer.from(a);
@@ -12,6 +13,7 @@ export class WebAuth {
12
13
  _host;
13
14
  now;
14
15
  devices;
16
+ access;
15
17
  _bootstrapSecret = token(32);
16
18
  bootstrapExpiresAt = Date.now() + 5 * 60_000;
17
19
  bootstrapUsed = false;
@@ -20,18 +22,25 @@ export class WebAuth {
20
22
  tickets = new Map();
21
23
  rates = new Map();
22
24
  sockets = new Map();
23
- constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore()) {
25
+ constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }) {
24
26
  this._origin = _origin;
25
27
  this._host = _host;
26
28
  this.now = now;
27
29
  this.devices = devices;
30
+ this.access = access;
28
31
  }
29
32
  get bootstrapSecret() { return this._bootstrapSecret; }
30
33
  get origin() { return this._origin; }
31
34
  get host() { return this._host; }
32
- setBoundary(origin, host) {
35
+ get mode() { return this.access.mode; }
36
+ get secureCookies() { return this._origin.startsWith('https:'); }
37
+ allowedOrigins = new Set();
38
+ allowedHosts = new Set();
39
+ setBoundary(origin, host, aliases = {}) {
33
40
  this._origin = origin;
34
41
  this._host = host;
42
+ this.allowedOrigins = new Set([origin, ...(aliases.origins ?? [])]);
43
+ this.allowedHosts = new Set([host, ...(aliases.hosts ?? [])]);
35
44
  }
36
45
  /** Mint a replacement for an operator-triggered reauthentication ceremony. */
37
46
  mintBootstrap() {
@@ -42,16 +51,20 @@ export class WebAuth {
42
51
  }
43
52
  validateBoundary(request, requireOrigin) {
44
53
  const host = request.headers.host;
45
- if (host !== this.host)
46
- throw new FleetError('forbidden', 'invalid Host header');
47
- if (requireOrigin && request.headers.origin !== this.origin)
48
- throw new FleetError('forbidden', 'invalid Origin header');
54
+ const hosts = this.allowedHosts.size ? this.allowedHosts : new Set([this.host]);
55
+ const origins = this.allowedOrigins.size ? this.allowedOrigins : new Set([this.origin]);
56
+ if (!host || !hosts.has(host))
57
+ throw new FleetError('forbidden', `This address is not configured for the fleet console. Open ${this.origin} or set --public-origin.`);
58
+ if (requireOrigin && (!request.headers.origin || !origins.has(request.headers.origin)))
59
+ throw new FleetError('forbidden', 'request Origin does not match the configured control-panel origin');
49
60
  const fetchSite = request.headers['sec-fetch-site'];
50
61
  if (fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
51
62
  throw new FleetError('forbidden', 'cross-site request rejected');
52
63
  }
53
64
  exchange(request) {
54
65
  this.validateBoundary(request, true);
66
+ if (this.access.mode !== 'pairing')
67
+ throw new FleetError('forbidden', 'one-time pairing is disabled for this access mode');
55
68
  this.consumeRate('bootstrap', 10, 60_000);
56
69
  const authorization = request.headers.authorization ?? '';
57
70
  const supplied = authorization.startsWith('Bootstrap ') ? authorization.slice(10) : '';
@@ -61,8 +74,24 @@ export class WebAuth {
61
74
  const device = this.devices.issue();
62
75
  return { session: this.createSession(device.id), device };
63
76
  }
77
+ login(request, password) {
78
+ this.validateBoundary(request, true);
79
+ this.consumeRate('password-login', 10, 60_000);
80
+ if (!verifyPassword(this.access, password))
81
+ throw new FleetError('unauthorized', 'invalid control-panel password');
82
+ const device = this.devices.issue();
83
+ return { session: this.createSession(device.id), device };
84
+ }
85
+ anonymous(request) {
86
+ this.validateBoundary(request, true);
87
+ if (this.access.mode !== 'none')
88
+ throw new FleetError('forbidden', 'unprotected access is disabled');
89
+ return this.createSession('unprotected');
90
+ }
64
91
  resume(request) {
65
92
  this.validateBoundary(request, true);
93
+ if (this.access.mode === 'none')
94
+ throw new FleetError('forbidden', 'trusted devices are disabled');
66
95
  this.consumeRate('device-resume', 30, 60_000);
67
96
  const current = parseCookies(request.headers.cookie ?? '').ofs_device;
68
97
  const device = current ? this.devices.rotate(current) : undefined;
@@ -92,7 +121,7 @@ export class WebAuth {
92
121
  logout(request) {
93
122
  const session = this.authenticate(request, true);
94
123
  const deviceId = this.sessionDevices.get(session.id);
95
- if (deviceId)
124
+ if (deviceId && deviceId !== 'unprotected')
96
125
  this.devices.revokeId(deviceId);
97
126
  this.removeSession(session.id);
98
127
  }
@@ -1,10 +1,14 @@
1
1
  import { type WebServer } from './server.js';
2
+ import { type WebAccessConfig } from './access.js';
2
3
  export interface StartWebOptions {
3
4
  configPath?: string;
4
5
  port?: number;
5
6
  open?: boolean;
6
7
  binPath: string;
7
8
  log?(line: string): void;
9
+ bind?: string;
10
+ publicOrigin?: string;
11
+ access?: WebAccessConfig;
8
12
  }
9
13
  export interface RunningWebConsole extends WebServer {
10
14
  address: string;
@@ -22,6 +22,7 @@ import { acquireWebServerLock } from './lock.js';
22
22
  import { TrustedDeviceStore } from './device-store.js';
23
23
  import { WebAuth } from './auth.js';
24
24
  import { startWebControlServer } from './control.js';
25
+ import { WebAccessStore, validatePublicOrigin } from './access.js';
25
26
  import { buildWatchdogFindings, cachedWatchdogFindingsProvider, WatchdogQueryService } from '../watchdog/query.js';
26
27
  import { latestReport } from '../watchdog/store.js';
27
28
  const CONFIG_CACHE_TTL_MS = 5_000;
@@ -46,7 +47,14 @@ export async function startWebConsole(options) {
46
47
  throw new FleetError('invalid_request', 'port must be between 0 and 65535');
47
48
  const lock = acquireWebServerLock();
48
49
  const webDir = resolve(stateRoot(), 'web');
49
- const auth = new WebAuth(`http://127.0.0.1:${requestedPort}`, `127.0.0.1:${requestedPort}`, Date.now, new TrustedDeviceStore(webDir));
50
+ const bind = options.bind ?? '127.0.0.1';
51
+ const publicOrigin = options.publicOrigin ? validatePublicOrigin(options.publicOrigin) : undefined;
52
+ if (!isLoopback(bind) && !publicOrigin) {
53
+ lock.release();
54
+ throw new FleetError('forbidden', 'a non-loopback bind requires an explicit --public-origin');
55
+ }
56
+ const access = options.access ?? new WebAccessStore(webDir).read();
57
+ const auth = new WebAuth(publicOrigin?.origin ?? `http://127.0.0.1:${requestedPort}`, publicOrigin?.host ?? `127.0.0.1:${requestedPort}`, Date.now, new TrustedDeviceStore(webDir), access);
50
58
  const tmux = new Tmux();
51
59
  const backend = pickBackend();
52
60
  const repository = new RoleRepository({
@@ -157,7 +165,7 @@ export async function startWebConsole(options) {
157
165
  }
158
166
  let address;
159
167
  try {
160
- address = await server.app.listen({ host: '127.0.0.1', port: requestedPort });
168
+ address = await server.app.listen({ host: bind, port: requestedPort });
161
169
  }
162
170
  catch (error) {
163
171
  await server.close();
@@ -165,19 +173,24 @@ export async function startWebConsole(options) {
165
173
  throw error;
166
174
  }
167
175
  const actual = new URL(address);
168
- if (actual.hostname !== '127.0.0.1') {
169
- await server.close();
170
- lock.release();
171
- throw new FleetError('forbidden', 'web console refused a non-loopback bind');
172
- }
173
- const host = `127.0.0.1:${actual.port}`;
174
- server.auth.setBoundary(`http://${host}`, host);
176
+ const localHost = `127.0.0.1:${actual.port}`;
177
+ const browserOrigin = publicOrigin?.origin ?? `http://${localHost}`;
178
+ const browserHost = publicOrigin?.host ?? localHost;
179
+ server.auth.setBoundary(browserOrigin, browserHost, publicOrigin ? {
180
+ // nginx's safe default uses the loopback upstream as Host. The declared
181
+ // browser Origin remains mandatory for auth/mutations and WebSocket hello,
182
+ // so operators do not need a fragile Host-rewrite incantation.
183
+ hosts: [localHost, `localhost:${actual.port}`],
184
+ } : {
185
+ hosts: [`localhost:${actual.port}`], origins: [`http://localhost:${actual.port}`],
186
+ });
175
187
  let control;
176
188
  try {
177
189
  control = await startWebControlServer({
178
190
  dir: webDir,
179
191
  onOpen() {
180
- const url = `http://${host}/#bootstrap=${server.auth.mintBootstrap()}`;
192
+ const url = access.mode === 'pairing'
193
+ ? `${browserOrigin}/#bootstrap=${server.auth.mintBootstrap()}` : `${browserOrigin}/`;
181
194
  openBrowser(url);
182
195
  },
183
196
  onRevokeAll() { server.auth.revokeAllTrustedDevices(); },
@@ -189,9 +202,10 @@ export async function startWebConsole(options) {
189
202
  throw error;
190
203
  }
191
204
  if (options.open !== false)
192
- openBrowser(`http://${host}/#bootstrap=${server.auth.bootstrapSecret}`);
205
+ openBrowser(access.mode === 'pairing'
206
+ ? `${browserOrigin}/#bootstrap=${server.auth.bootstrapSecret}` : `${browserOrigin}/`);
193
207
  return {
194
- ...server, address: `http://${host}`,
208
+ ...server, address: browserOrigin,
195
209
  async close() {
196
210
  try {
197
211
  await control.close();
@@ -204,6 +218,9 @@ export async function startWebConsole(options) {
204
218
  },
205
219
  };
206
220
  }
221
+ function isLoopback(host) {
222
+ return ['127.0.0.1', 'localhost', '::1'].includes(host);
223
+ }
207
224
  function openBrowser(url) {
208
225
  const command = process.platform === 'darwin' ? 'open'
209
226
  : process.platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -33,12 +33,19 @@ export async function buildWebServer(services, boundary, options = {}) {
33
33
  reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
34
34
  reply.header('X-Frame-Options', 'DENY');
35
35
  reply.header('Content-Security-Policy', `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; ` +
36
- `connect-src 'self' ws://${auth.host}; object-src 'none'; base-uri 'none'; ` +
36
+ `connect-src 'self' ${auth.secureCookies ? 'wss' : 'ws'}://${auth.host}; object-src 'none'; base-uri 'none'; ` +
37
37
  `frame-ancestors 'none'; form-action 'self'; manifest-src 'self'; worker-src 'self'`);
38
38
  if (request.url.startsWith('/api/'))
39
39
  reply.header('Cache-Control', 'no-store');
40
- if (request.headers.host !== auth.host)
41
- throw new FleetError('forbidden', 'invalid Host header');
40
+ try {
41
+ auth.validateBoundary(request, false);
42
+ }
43
+ catch (error) {
44
+ if (request.url.startsWith('/api/'))
45
+ throw error;
46
+ const message = normalizeError(error).message;
47
+ return reply.code(421).type('text/html').send(`<!doctype html><html><head><title>Fleet console address</title></head><body><main><h1>This fleet-console address is not configured</h1><p>${escapeHtml(message)}</p><p>For nginx or VPS access, configure an explicit public origin. The console does not guess proxy hosts.</p></main></body></html>`);
48
+ }
42
49
  });
43
50
  app.setErrorHandler(async (error, request, reply) => {
44
51
  const fleetError = normalizeError(error, request.id);
@@ -50,13 +57,29 @@ export async function buildWebServer(services, boundary, options = {}) {
50
57
  });
51
58
  app.post('/api/v1/auth/exchange', async (request, reply) => {
52
59
  const { session, device } = auth.exchange(request);
53
- setAuthCookies(reply, session.id, device.token);
60
+ setAuthCookies(reply, session.id, device.token, auth.secureCookies);
54
61
  await audit.record({ requestId: request.id, browser: session.id, action: 'auth.exchange', result: 'succeeded' });
55
62
  return { csrfToken: session.csrf, expiresAt: new Date(session.absoluteExpiresAt).toISOString() };
56
63
  });
64
+ app.get('/api/v1/auth/mode', async () => ({
65
+ mode: auth.mode,
66
+ warning: auth.mode === 'none'
67
+ ? 'Unprotected mode: anyone who can reach this address can control the fleet.' : undefined,
68
+ }));
69
+ app.post('/api/v1/auth/login', async (request, reply) => {
70
+ const { session, device } = auth.login(request, String(request.body?.password ?? ''));
71
+ setAuthCookies(reply, session.id, device.token, auth.secureCookies);
72
+ await audit.record({ requestId: request.id, browser: session.id, action: 'auth.password', result: 'succeeded' });
73
+ return { csrfToken: session.csrf, expiresAt: new Date(session.absoluteExpiresAt).toISOString() };
74
+ });
75
+ app.post('/api/v1/auth/anonymous', async (request, reply) => {
76
+ const session = auth.anonymous(request);
77
+ setSessionCookie(reply, session.id, auth.secureCookies);
78
+ return { csrfToken: session.csrf, expiresAt: new Date(session.absoluteExpiresAt).toISOString() };
79
+ });
57
80
  app.post('/api/v1/auth/resume', async (request, reply) => {
58
81
  const { session, device } = auth.resume(request);
59
- setAuthCookies(reply, session.id, device.token);
82
+ setAuthCookies(reply, session.id, device.token, auth.secureCookies);
60
83
  await audit.record({ requestId: request.id, browser: session.id, action: 'auth.resume', result: 'succeeded' });
61
84
  return { csrfToken: session.csrf, expiresAt: new Date(session.absoluteExpiresAt).toISOString() };
62
85
  });
@@ -243,12 +266,16 @@ export async function buildWebServer(services, boundary, options = {}) {
243
266
  },
244
267
  };
245
268
  }
246
- function setAuthCookies(reply, session, device) {
269
+ function setAuthCookies(reply, session, device, secure = false) {
270
+ const suffix = secure ? '; Secure' : '';
247
271
  reply.header('Set-Cookie', [
248
- `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=28800`,
249
- `ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=2592000`,
272
+ `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=28800${suffix}`,
273
+ `ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=2592000${suffix}`,
250
274
  ]);
251
275
  }
276
+ function setSessionCookie(reply, session, secure = false) {
277
+ reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/api; Max-Age=28800${secure ? '; Secure' : ''}`);
278
+ }
252
279
  function clearAuthCookies(reply) {
253
280
  reply.header('Set-Cookie', [
254
281
  'ofs_session=; HttpOnly; SameSite=Strict; Path=/api; Max-Age=0',
@@ -256,6 +283,9 @@ function clearAuthCookies(reply) {
256
283
  ]);
257
284
  }
258
285
  function cryptoRandomId() { return randomBytes(12).toString('hex'); }
286
+ function escapeHtml(value) {
287
+ return value.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
288
+ }
259
289
  function requireSubprotocol(request, expected) {
260
290
  const protocols = String(request.headers['sec-websocket-protocol'] ?? '')
261
291
  .split(',').map(value => value.trim());
@@ -2,12 +2,14 @@ import { type Exec } from '../exec.js';
2
2
  export declare const WEB_SYSTEMD_UNIT = "ours-fleet-web.service";
3
3
  export declare const WEB_LAUNCHD_LABEL = "network.ours.fleet.web";
4
4
  interface ServiceMetadata {
5
- version: 2;
5
+ version: 3;
6
6
  platform: 'linux' | 'darwin';
7
7
  runtime: string;
8
8
  script: string;
9
9
  port: number;
10
10
  configuration?: string;
11
+ bind?: string;
12
+ publicOrigin?: string;
11
13
  }
12
14
  export interface WebServiceOptions {
13
15
  platform?: NodeJS.Platform;
@@ -27,7 +29,10 @@ export declare class WebServiceManager {
27
29
  constructor(options?: WebServiceOptions);
28
30
  get metadataPath(): string;
29
31
  get definitionPath(): string;
30
- install(script: string, port?: number, configuration?: string): Promise<string[]>;
32
+ install(script: string, port?: number, configuration?: string, web?: {
33
+ bind?: string;
34
+ publicOrigin?: string;
35
+ }): Promise<string[]>;
31
36
  start(): Promise<void>;
32
37
  stop(): Promise<void>;
33
38
  restart(): Promise<void>;
@@ -37,6 +42,12 @@ export declare class WebServiceManager {
37
42
  private requireInstalled;
38
43
  private must;
39
44
  }
40
- export declare function systemdUnit(runtime: string, script: string, port: number, configuration?: string): string;
41
- export declare function launchdPlist(runtime: string, script: string, port: number, configuration?: string): string;
45
+ export declare function systemdUnit(runtime: string, script: string, port: number, configuration?: string, web?: {
46
+ bind?: string;
47
+ publicOrigin?: string;
48
+ }): string;
49
+ export declare function launchdPlist(runtime: string, script: string, port: number, configuration?: string, web?: {
50
+ bind?: string;
51
+ publicOrigin?: string;
52
+ }): string;
42
53
  export {};
@@ -5,7 +5,7 @@ import { replaceFileAtomically } from '../atomic-file.js';
5
5
  import { FleetError } from '../application/errors.js';
6
6
  import { realExec } from '../exec.js';
7
7
  import { home, stateRoot } from '../paths.js';
8
- const VERSION = 2;
8
+ const VERSION = 3;
9
9
  export const WEB_SYSTEMD_UNIT = 'ours-fleet-web.service';
10
10
  export const WEB_LAUNCHD_LABEL = 'network.ours.fleet.web';
11
11
  export class WebServiceManager {
@@ -32,7 +32,7 @@ export class WebServiceManager {
32
32
  ? join(this.homeDir, '.config', 'systemd', 'user', WEB_SYSTEMD_UNIT)
33
33
  : join(this.homeDir, 'Library', 'LaunchAgents', `${WEB_LAUNCHD_LABEL}.plist`);
34
34
  }
35
- async install(script, port = 49_271, configuration) {
35
+ async install(script, port = 49_271, configuration, web = {}) {
36
36
  validatePort(port);
37
37
  const resolvedScript = resolveExecutable(script, 'web CLI script');
38
38
  const runtime = resolveExecutable(this.runtimeExecutable, 'Node runtime');
@@ -43,10 +43,12 @@ export class WebServiceManager {
43
43
  const metadata = {
44
44
  version: VERSION, platform: this.platform, runtime, script: resolvedScript, port,
45
45
  ...(config ? { configuration: config } : {}),
46
+ ...(web.bind ? { bind: web.bind } : {}),
47
+ ...(web.publicOrigin ? { publicOrigin: web.publicOrigin } : {}),
46
48
  };
47
49
  replaceFileAtomically(this.definitionPath, this.platform === 'linux'
48
- ? systemdUnit(runtime, resolvedScript, port, config)
49
- : launchdPlist(runtime, resolvedScript, port, config), 0o600);
50
+ ? systemdUnit(runtime, resolvedScript, port, config, web)
51
+ : launchdPlist(runtime, resolvedScript, port, config, web), 0o600);
50
52
  replaceFileAtomically(this.metadataPath, JSON.stringify(metadata, null, 2) + '\n', 0o600);
51
53
  if (this.platform === 'linux') {
52
54
  await this.must('systemctl', ['--user', 'daemon-reload']);
@@ -123,9 +125,9 @@ export class WebServiceManager {
123
125
  readMetadata() {
124
126
  try {
125
127
  const parsed = JSON.parse(readFileSync(this.metadataPath, 'utf8'));
126
- return parsed.version === VERSION && parsed.platform === this.platform
128
+ return [2, VERSION].includes(parsed.version) && parsed.platform === this.platform
127
129
  && typeof parsed.runtime === 'string' && typeof parsed.script === 'string'
128
- && Number.isInteger(parsed.port) ? parsed : undefined;
130
+ && Number.isInteger(parsed.port) ? { ...parsed, version: VERSION } : undefined;
129
131
  }
130
132
  catch {
131
133
  return undefined;
@@ -157,24 +159,28 @@ function validatePort(port) {
157
159
  function systemdQuote(value) {
158
160
  return `"${value.replace(/[%\\"]/g, char => char === '%' ? '%%' : `\\${char}`)}"`;
159
161
  }
160
- export function systemdUnit(runtime, script, port, configuration) {
162
+ export function systemdUnit(runtime, script, port, configuration, web = {}) {
161
163
  const config = configuration ? ` --configuration ${systemdQuote(configuration)}` : '';
164
+ const access = `${web.bind ? ` --bind ${systemdQuote(web.bind)}` : ''}`
165
+ + `${web.publicOrigin ? ` --public-origin ${systemdQuote(web.publicOrigin)}` : ''}`;
162
166
  return `[Unit]\nDescription=ours-fleet localhost web console\nAfter=default.target\n\n`
163
- + `[Service]\nType=simple\nExecStart=${systemdQuote(runtime)} ${systemdQuote(script)} web serve --port ${port} --no-open${config}\n`
167
+ + `[Service]\nType=simple\nExecStart=${systemdQuote(runtime)} ${systemdQuote(script)} web serve --port ${port} --no-open${config}${access}\n`
164
168
  + `Restart=on-failure\nRestartSec=5\nTimeoutStopSec=15\n\n`
165
169
  + `[Install]\nWantedBy=default.target\n`;
166
170
  }
167
171
  const xml = (value) => value.replace(/[&<>"']/g, char => ({
168
172
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;',
169
173
  }[char]));
170
- export function launchdPlist(runtime, script, port, configuration) {
174
+ export function launchdPlist(runtime, script, port, configuration, web = {}) {
171
175
  const config = configuration
172
176
  ? `<string>--configuration</string><string>${xml(configuration)}</string>` : '';
177
+ const access = `${web.bind ? `<string>--bind</string><string>${xml(web.bind)}</string>` : ''}`
178
+ + `${web.publicOrigin ? `<string>--public-origin</string><string>${xml(web.publicOrigin)}</string>` : ''}`;
173
179
  return `<?xml version="1.0" encoding="UTF-8"?>\n`
174
180
  + `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n`
175
181
  + `<plist version="1.0"><dict>\n<key>Label</key><string>${WEB_LAUNCHD_LABEL}</string>\n`
176
182
  + `<key>ProgramArguments</key><array><string>${xml(runtime)}</string><string>${xml(script)}</string><string>web</string>`
177
- + `<string>serve</string><string>--port</string><string>${port}</string><string>--no-open</string>${config}</array>\n`
183
+ + `<string>serve</string><string>--port</string><string>${port}</string><string>--no-open</string>${config}${access}</array>\n`
178
184
  + `<key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n`
179
185
  + `<key>ProcessType</key><string>Background</string>\n</dict></plist>\n`;
180
186
  }
@@ -1,4 +1,4 @@
1
- import{r as le,a as Ee,j as re}from"./index-CUN7ksTw.js";var ge={exports:{}},Se;function ke(){return Se||(Se=1,(function(se,ne){(function(Q,X){se.exports=X()})(globalThis,(()=>(()=>{var Q={4567:function(B,r,o){var l=this&&this.__decorate||function(e,i,a,v){var f,g=arguments.length,c=g<3?i:v===null?v=Object.getOwnPropertyDescriptor(i,a):v;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")c=Reflect.decorate(e,i,a,v);else for(var m=e.length-1;m>=0;m--)(f=e[m])&&(c=(g<3?f(c):g>3?f(i,a,c):f(i,a))||c);return g>3&&c&&Object.defineProperty(i,a,c),c},_=this&&this.__param||function(e,i){return function(a,v){i(a,v,e)}};Object.defineProperty(r,"__esModule",{value:!0}),r.AccessibilityManager=void 0;const n=o(9042),d=o(9924),u=o(844),p=o(4725),h=o(2585),t=o(3656);let s=r.AccessibilityManager=class extends u.Disposable{constructor(e,i,a,v){super(),this._terminal=e,this._coreBrowserService=a,this._renderService=v,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let f=0;f<this._terminal.rows;f++)this._rowElements[f]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[f]);if(this._topBoundaryFocusListener=f=>this._handleBoundaryFocus(f,0),this._bottomBoundaryFocusListener=f=>this._handleBoundaryFocus(f,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new d.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((f=>this._handleResize(f.rows)))),this.register(this._terminal.onRender((f=>this._refreshRows(f.start,f.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((f=>this._handleChar(f)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(`
1
+ import{r as le,a as Ee,j as re}from"./index-B6T8JLSd.js";var ge={exports:{}},Se;function ke(){return Se||(Se=1,(function(se,ne){(function(Q,X){se.exports=X()})(globalThis,(()=>(()=>{var Q={4567:function(B,r,o){var l=this&&this.__decorate||function(e,i,a,v){var f,g=arguments.length,c=g<3?i:v===null?v=Object.getOwnPropertyDescriptor(i,a):v;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")c=Reflect.decorate(e,i,a,v);else for(var m=e.length-1;m>=0;m--)(f=e[m])&&(c=(g<3?f(c):g>3?f(i,a,c):f(i,a))||c);return g>3&&c&&Object.defineProperty(i,a,c),c},_=this&&this.__param||function(e,i){return function(a,v){i(a,v,e)}};Object.defineProperty(r,"__esModule",{value:!0}),r.AccessibilityManager=void 0;const n=o(9042),d=o(9924),u=o(844),p=o(4725),h=o(2585),t=o(3656);let s=r.AccessibilityManager=class extends u.Disposable{constructor(e,i,a,v){super(),this._terminal=e,this._coreBrowserService=a,this._renderService=v,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let f=0;f<this._terminal.rows;f++)this._rowElements[f]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[f]);if(this._topBoundaryFocusListener=f=>this._handleBoundaryFocus(f,0),this._bottomBoundaryFocusListener=f=>this._handleBoundaryFocus(f,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new d.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((f=>this._handleResize(f.rows)))),this.register(this._terminal.onRender((f=>this._refreshRows(f.start,f.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((f=>this._handleChar(f)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(`
2
2
  `)))),this.register(this._terminal.onA11yTab((f=>this._handleTab(f)))),this.register(this._terminal.onKey((f=>this._handleKey(f.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,t.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,u.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(e){for(let i=0;i<e;i++)this._handleChar(" ")}_handleChar(e){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,e===`
3
3
  `&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=n.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(e){this._clearLiveRegion(),new RegExp("\\p{Control}","u").test(e)||this._charsToConsume.push(e)}_refreshRows(e,i){this._liveRegionDebouncer.refresh(e,i,this._terminal.rows)}_renderRows(e,i){const a=this._terminal.buffer,v=a.lines.length.toString();for(let f=e;f<=i;f++){const g=a.lines.get(a.ydisp+f),c=[],m=g?.translateToString(!0,void 0,void 0,c)||"",E=(a.ydisp+f+1).toString(),k=this._rowElements[f];k&&(m.length===0?(k.innerText=" ",this._rowColumns.set(k,[0,1])):(k.textContent=m,this._rowColumns.set(k,c)),k.setAttribute("aria-posinset",E),k.setAttribute("aria-setsize",v))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(e,i){const a=e.target,v=this._rowElements[i===0?1:this._rowElements.length-2];if(a.getAttribute("aria-posinset")===(i===0?"1":`${this._terminal.buffer.lines.length}`)||e.relatedTarget!==v)return;let f,g;if(i===0?(f=a,g=this._rowElements.pop(),this._rowContainer.removeChild(g)):(f=this._rowElements.shift(),g=a,this._rowContainer.removeChild(f)),f.removeEventListener("focus",this._topBoundaryFocusListener),g.removeEventListener("focus",this._bottomBoundaryFocusListener),i===0){const c=this._createAccessibilityTreeNode();this._rowElements.unshift(c),this._rowContainer.insertAdjacentElement("afterbegin",c)}else{const c=this._createAccessibilityTreeNode();this._rowElements.push(c),this._rowContainer.appendChild(c)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(i===0?-1:1),this._rowElements[i===0?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_handleSelectionChange(){if(this._rowElements.length===0)return;const e=document.getSelection();if(!e)return;if(e.isCollapsed)return void(this._rowContainer.contains(e.anchorNode)&&this._terminal.clearSelection());if(!e.anchorNode||!e.focusNode)return void console.error("anchorNode and/or focusNode are null");let i={node:e.anchorNode,offset:e.anchorOffset},a={node:e.focusNode,offset:e.focusOffset};if((i.node.compareDocumentPosition(a.node)&Node.DOCUMENT_POSITION_PRECEDING||i.node===a.node&&i.offset>a.offset)&&([i,a]=[a,i]),i.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(i={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(i.node))return;const v=this._rowElements.slice(-1)[0];if(a.node.compareDocumentPosition(v)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(a={node:v,offset:v.textContent?.length??0}),!this._rowContainer.contains(a.node))return;const f=({node:m,offset:E})=>{const k=m instanceof Text?m.parentNode:m;let D=parseInt(k?.getAttribute("aria-posinset"),10)-1;if(isNaN(D))return console.warn("row is invalid. Race condition?"),null;const b=this._rowColumns.get(k);if(!b)return console.warn("columns is null. Race condition?"),null;let x=E<b.length?b[E]:b.slice(-1)[0]+1;return x>=this._terminal.cols&&(++D,x=0),{row:D,column:x}},g=f(i),c=f(a);if(g&&c){if(g.row>c.row||g.row===c.row&&g.column>=c.column)throw new Error("invalid range");this._terminal.select(g.column,g.row,(c.row-g.row)*this._terminal.cols-g.column+c.column)}}_handleResize(e){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let i=this._rowContainer.children.length;i<this._terminal.rows;i++)this._rowElements[i]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[i]);for(;this._rowElements.length>e;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const e=this._coreBrowserService.mainDocument.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=`${this._renderService.dimensions.css.canvas.width}px`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let e=0;e<this._terminal.rows;e++)this._refreshRowDimensions(this._rowElements[e])}}_refreshRowDimensions(e){e.style.height=`${this._renderService.dimensions.css.cell.height}px`}};r.AccessibilityManager=s=l([_(1,h.IInstantiationService),_(2,p.ICoreBrowserService),_(3,p.IRenderService)],s)},3614:(B,r)=>{function o(d){return d.replace(/\r?\n/g,"\r")}function l(d,u){return u?"\x1B[200~"+d+"\x1B[201~":d}function _(d,u,p,h){d=l(d=o(d),p.decPrivateModes.bracketedPasteMode&&h.rawOptions.ignoreBracketedPasteMode!==!0),p.triggerDataEvent(d,!0),u.value=""}function n(d,u,p){const h=p.getBoundingClientRect(),t=d.clientX-h.left-10,s=d.clientY-h.top-10;u.style.width="20px",u.style.height="20px",u.style.left=`${t}px`,u.style.top=`${s}px`,u.style.zIndex="1000",u.focus()}Object.defineProperty(r,"__esModule",{value:!0}),r.rightClickHandler=r.moveTextAreaUnderMouseCursor=r.paste=r.handlePasteEvent=r.copyHandler=r.bracketTextForPaste=r.prepareTextForTerminal=void 0,r.prepareTextForTerminal=o,r.bracketTextForPaste=l,r.copyHandler=function(d,u){d.clipboardData&&d.clipboardData.setData("text/plain",u.selectionText),d.preventDefault()},r.handlePasteEvent=function(d,u,p,h){d.stopPropagation(),d.clipboardData&&_(d.clipboardData.getData("text/plain"),u,p,h)},r.paste=_,r.moveTextAreaUnderMouseCursor=n,r.rightClickHandler=function(d,u,p,h,t){n(d,u,p),t&&h.rightClickSelect(d),u.value=h.selectionText,u.select()}},7239:(B,r,o)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ColorContrastCache=void 0;const l=o(1505);r.ColorContrastCache=class{constructor(){this._color=new l.TwoKeyMap,this._css=new l.TwoKeyMap}setCss(_,n,d){this._css.set(_,n,d)}getCss(_,n){return this._css.get(_,n)}setColor(_,n,d){this._color.set(_,n,d)}getColor(_,n){return this._color.get(_,n)}clear(){this._color.clear(),this._css.clear()}}},3656:(B,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.addDisposableDomListener=void 0,r.addDisposableDomListener=function(o,l,_,n){o.addEventListener(l,_,n);let d=!1;return{dispose:()=>{d||(d=!0,o.removeEventListener(l,_,n))}}}},3551:function(B,r,o){var l=this&&this.__decorate||function(s,e,i,a){var v,f=arguments.length,g=f<3?e:a===null?a=Object.getOwnPropertyDescriptor(e,i):a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")g=Reflect.decorate(s,e,i,a);else for(var c=s.length-1;c>=0;c--)(v=s[c])&&(g=(f<3?v(g):f>3?v(e,i,g):v(e,i))||g);return f>3&&g&&Object.defineProperty(e,i,g),g},_=this&&this.__param||function(s,e){return function(i,a){e(i,a,s)}};Object.defineProperty(r,"__esModule",{value:!0}),r.Linkifier=void 0;const n=o(3656),d=o(8460),u=o(844),p=o(2585),h=o(4725);let t=r.Linkifier=class extends u.Disposable{get currentLink(){return this._currentLink}constructor(s,e,i,a,v){super(),this._element=s,this._mouseService=e,this._renderService=i,this._bufferService=a,this._linkProviderService=v,this._linkCacheDisposables=[],this._isMouseOut=!0,this._wasResized=!1,this._activeLine=-1,this._onShowLinkUnderline=this.register(new d.EventEmitter),this.onShowLinkUnderline=this._onShowLinkUnderline.event,this._onHideLinkUnderline=this.register(new d.EventEmitter),this.onHideLinkUnderline=this._onHideLinkUnderline.event,this.register((0,u.getDisposeArrayDisposable)(this._linkCacheDisposables)),this.register((0,u.toDisposable)((()=>{this._lastMouseEvent=void 0,this._activeProviderReplies?.clear()}))),this.register(this._bufferService.onResize((()=>{this._clearCurrentLink(),this._wasResized=!0}))),this.register((0,n.addDisposableDomListener)(this._element,"mouseleave",(()=>{this._isMouseOut=!0,this._clearCurrentLink()}))),this.register((0,n.addDisposableDomListener)(this._element,"mousemove",this._handleMouseMove.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,"mousedown",this._handleMouseDown.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,"mouseup",this._handleMouseUp.bind(this)))}_handleMouseMove(s){this._lastMouseEvent=s;const e=this._positionFromMouseEvent(s,this._element,this._mouseService);if(!e)return;this._isMouseOut=!1;const i=s.composedPath();for(let a=0;a<i.length;a++){const v=i[a];if(v.classList.contains("xterm"))break;if(v.classList.contains("xterm-hover"))return}this._lastBufferCell&&e.x===this._lastBufferCell.x&&e.y===this._lastBufferCell.y||(this._handleHover(e),this._lastBufferCell=e)}_handleHover(s){if(this._activeLine!==s.y||this._wasResized)return this._clearCurrentLink(),this._askForLink(s,!1),void(this._wasResized=!1);this._currentLink&&this._linkAtPosition(this._currentLink.link,s)||(this._clearCurrentLink(),this._askForLink(s,!0))}_askForLink(s,e){this._activeProviderReplies&&e||(this._activeProviderReplies?.forEach((a=>{a?.forEach((v=>{v.link.dispose&&v.link.dispose()}))})),this._activeProviderReplies=new Map,this._activeLine=s.y);let i=!1;for(const[a,v]of this._linkProviderService.linkProviders.entries())e?this._activeProviderReplies?.get(a)&&(i=this._checkLinkProviderResult(a,s,i)):v.provideLinks(s.y,(f=>{if(this._isMouseOut)return;const g=f?.map((c=>({link:c})));this._activeProviderReplies?.set(a,g),i=this._checkLinkProviderResult(a,s,i),this._activeProviderReplies?.size===this._linkProviderService.linkProviders.length&&this._removeIntersectingLinks(s.y,this._activeProviderReplies)}))}_removeIntersectingLinks(s,e){const i=new Set;for(let a=0;a<e.size;a++){const v=e.get(a);if(v)for(let f=0;f<v.length;f++){const g=v[f],c=g.link.range.start.y<s?0:g.link.range.start.x,m=g.link.range.end.y>s?this._bufferService.cols:g.link.range.end.x;for(let E=c;E<=m;E++){if(i.has(E)){v.splice(f--,1);break}i.add(E)}}}}_checkLinkProviderResult(s,e,i){if(!this._activeProviderReplies)return i;const a=this._activeProviderReplies.get(s);let v=!1;for(let f=0;f<s;f++)this._activeProviderReplies.has(f)&&!this._activeProviderReplies.get(f)||(v=!0);if(!v&&a){const f=a.find((g=>this._linkAtPosition(g.link,e)));f&&(i=!0,this._handleNewLink(f))}if(this._activeProviderReplies.size===this._linkProviderService.linkProviders.length&&!i)for(let f=0;f<this._activeProviderReplies.size;f++){const g=this._activeProviderReplies.get(f)?.find((c=>this._linkAtPosition(c.link,e)));if(g){i=!0,this._handleNewLink(g);break}}return i}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(s){if(!this._currentLink)return;const e=this._positionFromMouseEvent(s,this._element,this._mouseService);e&&this._mouseDownLink===this._currentLink&&this._linkAtPosition(this._currentLink.link,e)&&this._currentLink.link.activate(s,this._currentLink.link.text)}_clearCurrentLink(s,e){this._currentLink&&this._lastMouseEvent&&(!s||!e||this._currentLink.link.range.start.y>=s&&this._currentLink.link.range.end.y<=e)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,(0,u.disposeArray)(this._linkCacheDisposables))}_handleNewLink(s){if(!this._lastMouseEvent)return;const e=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);e&&this._linkAtPosition(s.link,e)&&(this._currentLink=s,this._currentLink.state={decorations:{underline:s.link.decorations===void 0||s.link.decorations.underline,pointerCursor:s.link.decorations===void 0||s.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,s.link,this._lastMouseEvent),s.link.decorations={},Object.defineProperties(s.link.decorations,{pointerCursor:{get:()=>this._currentLink?.state?.decorations.pointerCursor,set:i=>{this._currentLink?.state&&this._currentLink.state.decorations.pointerCursor!==i&&(this._currentLink.state.decorations.pointerCursor=i,this._currentLink.state.isHovered&&this._element.classList.toggle("xterm-cursor-pointer",i))}},underline:{get:()=>this._currentLink?.state?.decorations.underline,set:i=>{this._currentLink?.state&&this._currentLink?.state?.decorations.underline!==i&&(this._currentLink.state.decorations.underline=i,this._currentLink.state.isHovered&&this._fireUnderlineEvent(s.link,i))}}}),this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange((i=>{if(!this._currentLink)return;const a=i.start===0?0:i.start+1+this._bufferService.buffer.ydisp,v=this._bufferService.buffer.ydisp+1+i.end;if(this._currentLink.link.range.start.y>=a&&this._currentLink.link.range.end.y<=v&&(this._clearCurrentLink(a,v),this._lastMouseEvent)){const f=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);f&&this._askForLink(f,!1)}}))))}_linkHover(s,e,i){this._currentLink?.state&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(e,!0),this._currentLink.state.decorations.pointerCursor&&s.classList.add("xterm-cursor-pointer")),e.hover&&e.hover(i,e.text)}_fireUnderlineEvent(s,e){const i=s.range,a=this._bufferService.buffer.ydisp,v=this._createLinkUnderlineEvent(i.start.x-1,i.start.y-a-1,i.end.x,i.end.y-a-1,void 0);(e?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(v)}_linkLeave(s,e,i){this._currentLink?.state&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(e,!1),this._currentLink.state.decorations.pointerCursor&&s.classList.remove("xterm-cursor-pointer")),e.leave&&e.leave(i,e.text)}_linkAtPosition(s,e){const i=s.range.start.y*this._bufferService.cols+s.range.start.x,a=s.range.end.y*this._bufferService.cols+s.range.end.x,v=e.y*this._bufferService.cols+e.x;return i<=v&&v<=a}_positionFromMouseEvent(s,e,i){const a=i.getCoords(s,e,this._bufferService.cols,this._bufferService.rows);if(a)return{x:a[0],y:a[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(s,e,i,a,v){return{x1:s,y1:e,x2:i,y2:a,cols:this._bufferService.cols,fg:v}}};r.Linkifier=t=l([_(1,h.IMouseService),_(2,h.IRenderService),_(3,p.IBufferService),_(4,h.ILinkProviderService)],t)},9042:(B,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.tooMuchOutput=r.promptLabel=void 0,r.promptLabel="Terminal input",r.tooMuchOutput="Too much output to announce, navigate to rows manually to read"},3730:function(B,r,o){var l=this&&this.__decorate||function(h,t,s,e){var i,a=arguments.length,v=a<3?t:e===null?e=Object.getOwnPropertyDescriptor(t,s):e;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")v=Reflect.decorate(h,t,s,e);else for(var f=h.length-1;f>=0;f--)(i=h[f])&&(v=(a<3?i(v):a>3?i(t,s,v):i(t,s))||v);return a>3&&v&&Object.defineProperty(t,s,v),v},_=this&&this.__param||function(h,t){return function(s,e){t(s,e,h)}};Object.defineProperty(r,"__esModule",{value:!0}),r.OscLinkProvider=void 0;const n=o(511),d=o(2585);let u=r.OscLinkProvider=class{constructor(h,t,s){this._bufferService=h,this._optionsService=t,this._oscLinkService=s}provideLinks(h,t){const s=this._bufferService.buffer.lines.get(h-1);if(!s)return void t(void 0);const e=[],i=this._optionsService.rawOptions.linkHandler,a=new n.CellData,v=s.getTrimmedLength();let f=-1,g=-1,c=!1;for(let m=0;m<v;m++)if(g!==-1||s.hasContent(m)){if(s.loadCell(m,a),a.hasExtendedAttrs()&&a.extended.urlId){if(g===-1){g=m,f=a.extended.urlId;continue}c=a.extended.urlId!==f}else g!==-1&&(c=!0);if(c||g!==-1&&m===v-1){const E=this._oscLinkService.getLinkData(f)?.uri;if(E){const k={start:{x:g+1,y:h},end:{x:m+(c||m!==v-1?0:1),y:h}};let D=!1;if(!i?.allowNonHttpProtocols)try{const b=new URL(E);["http:","https:"].includes(b.protocol)||(D=!0)}catch{D=!0}D||e.push({text:E,range:k,activate:(b,x)=>i?i.activate(b,x,k):p(0,x),hover:(b,x)=>i?.hover?.(b,x,k),leave:(b,x)=>i?.leave?.(b,x,k)})}c=!1,a.hasExtendedAttrs()&&a.extended.urlId?(g=m,f=a.extended.urlId):(g=-1,f=-1)}}t(e)}};function p(h,t){if(confirm(`Do you want to navigate to ${t}?
4
4