herdr-remote 0.2.3 → 0.2.5

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/tui.mjs CHANGED
@@ -78,6 +78,10 @@ var require_config = __commonJS({
78
78
  remoteUrl: "",
79
79
  maxPayloadBytes: 1024 * 1024,
80
80
  maxClientsPerHost: 16,
81
+ maxHosts: 1024,
82
+ maxPendingHandshakes: 1024,
83
+ maxBufferedBytesPerClient: 4 * 1024 * 1024,
84
+ hostReconnectGraceMs: 30 * 1e3,
81
85
  allowedOrigins: []
82
86
  },
83
87
  herdr: {
@@ -203,6 +207,20 @@ var require_config = __commonJS({
203
207
  config.relay.port = parseInteger(config.relay.port, DEFAULTS.relay.port, 1, 65535);
204
208
  config.relay.maxPayloadBytes = parseInteger(config.relay.maxPayloadBytes, DEFAULTS.relay.maxPayloadBytes, 4096, 16 * 1024 * 1024);
205
209
  config.relay.maxClientsPerHost = parseInteger(config.relay.maxClientsPerHost, DEFAULTS.relay.maxClientsPerHost, 1, 256);
210
+ config.relay.maxHosts = parseInteger(config.relay.maxHosts, DEFAULTS.relay.maxHosts, 1, 1e5);
211
+ config.relay.maxPendingHandshakes = parseInteger(config.relay.maxPendingHandshakes, DEFAULTS.relay.maxPendingHandshakes, 16, 1e5);
212
+ config.relay.maxBufferedBytesPerClient = parseInteger(
213
+ config.relay.maxBufferedBytesPerClient,
214
+ DEFAULTS.relay.maxBufferedBytesPerClient,
215
+ 64 * 1024,
216
+ 256 * 1024 * 1024
217
+ );
218
+ config.relay.hostReconnectGraceMs = parseInteger(
219
+ config.relay.hostReconnectGraceMs,
220
+ DEFAULTS.relay.hostReconnectGraceMs,
221
+ 1e3,
222
+ 24 * 60 * 60 * 1e3
223
+ );
206
224
  config.auth.pairingTtlMs = parseInteger(config.auth.pairingTtlMs, DEFAULTS.auth.pairingTtlMs, 30 * 1e3, 24 * 60 * 60 * 1e3);
207
225
  config.auth.deviceTtlMs = parseInteger(config.auth.deviceTtlMs, DEFAULTS.auth.deviceTtlMs, 60 * 1e3, 365 * 24 * 60 * 60 * 1e3);
208
226
  config.auth.maxDevices = parseInteger(config.auth.maxDevices, DEFAULTS.auth.maxDevices, 1, 1e4);
@@ -238,6 +256,10 @@ var require_config = __commonJS({
238
256
  if (env.RELAY_PORT) config.relay.port = env.RELAY_PORT;
239
257
  if (env.RELAY_PUBLIC_URL) config.relay.publicUrl = env.RELAY_PUBLIC_URL;
240
258
  if (env.RELAY_REMOTE_URL) config.relay.remoteUrl = env.RELAY_REMOTE_URL;
259
+ if (env.RELAY_MAX_HOSTS) config.relay.maxHosts = env.RELAY_MAX_HOSTS;
260
+ if (env.RELAY_MAX_PENDING_HANDSHAKES) config.relay.maxPendingHandshakes = env.RELAY_MAX_PENDING_HANDSHAKES;
261
+ if (env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT) config.relay.maxBufferedBytesPerClient = env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT;
262
+ if (env.RELAY_HOST_RECONNECT_GRACE_MS) config.relay.hostReconnectGraceMs = env.RELAY_HOST_RECONNECT_GRACE_MS;
241
263
  if (env.HERDR_SOCKET_PATH) config.herdr.socketPath = env.HERDR_SOCKET_PATH;
242
264
  if (env.HERDR_CWD) config.herdr.cwd = env.HERDR_CWD;
243
265
  if (env.HERDR_ARGS_JSON) {
@@ -360,7 +382,7 @@ var require_en = __commonJS({
360
382
  "use strict";
361
383
  module.exports = {
362
384
  "app.name": "Herdr Remote",
363
- "app.tagline": "Remote browser access to your Herdr workspaces",
385
+ "app.tagline": "Remote browser access to Herdr workspaces",
364
386
  "common.yes": "Yes",
365
387
  "common.no": "No",
366
388
  "common.on": "on",
@@ -393,12 +415,12 @@ var require_en = __commonJS({
393
415
  "nav.about": "Language & about",
394
416
  "mode.local": "This machine only",
395
417
  "mode.lan": "Local network / Tailscale",
396
- "mode.official": "Official relay (no server needed)",
418
+ "mode.official": "Official relay",
397
419
  "mode.remote": "Self-hosted relay",
398
- "mode.local.description": "The relay listens on 127.0.0.1. Only a browser on this machine can reach it.",
399
- "mode.lan.description": "The relay listens on 0.0.0.0 (every interface), so phones on your LAN or tailnet can reach it.",
400
- "mode.official.description": "Reach this machine from anywhere through the relay run by the Herdr Remote project. Nothing to deploy. Your terminal traffic passes through a server you do not control, and only devices you pair can open it.",
401
- "mode.remote.description": "No local relay. The workstation dials a relay you run, which is the only way in from outside your network.",
420
+ "mode.local.description": "Relay listens on 127.0.0.1. Local browser only.",
421
+ "mode.lan.description": "Relay listens on 0.0.0.0. Accessible over LAN or Tailscale.",
422
+ "mode.official.description": "Connect via official relay. No server needed.",
423
+ "mode.remote.description": "Connect via self-hosted relay for external access.",
402
424
  "overview.title": "Status",
403
425
  "overview.mode": "Access mode",
404
426
  "overview.relay": "Relay",
@@ -413,20 +435,20 @@ var require_en = __commonJS({
413
435
  "overview.relayRemote": "remote, {url}",
414
436
  "overview.socketMissing": "not found \u2014 is Herdr running?",
415
437
  "overview.unreachable": "unreachable: {message}",
416
- "overview.notStarted": "Services are not running. Open the Services tab to start them.",
438
+ "overview.notStarted": "Services not running. Open Services tab to start.",
417
439
  "pair.title": "Pair a device",
418
- "pair.generate": "Generate a one-time pairing code",
419
- "pair.regenerate": "Generate another code",
420
- "pair.working": "Starting services and requesting a code\u2026",
440
+ "pair.generate": "Generate pairing code",
441
+ "pair.regenerate": "Regenerate code",
442
+ "pair.working": "Generating pairing code\u2026",
421
443
  "pair.code": "Pairing code",
422
- "pair.url": "Open on your phone",
444
+ "pair.url": "Access URL",
423
445
  "pair.expires": "Expires in {minutes} (at {time})",
424
- "pair.expired": "This code has expired. Generate a new one.",
425
- "pair.instructions": "Open the URL, enter the code, and the browser is paired for good.",
426
- "pair.qrHint": "Scan the code with your phone camera to open the URL.",
427
- "pair.qrUnavailable": "The terminal is too narrow for a QR code; use the URL above.",
428
- "pair.failed": "Could not create a pairing code: {message}",
429
- "pair.hostOffline": "The relay has no workstation connected yet. Start the services first.",
446
+ "pair.expired": "Code expired. Generate a new one.",
447
+ "pair.instructions": "Open URL and enter code to pair.",
448
+ "pair.qrHint": "Scan QR code to open URL.",
449
+ "pair.qrUnavailable": "Terminal too narrow for QR code; use URL above.",
450
+ "pair.failed": "Could not create pairing code: {message}",
451
+ "pair.hostOffline": "Relay has no workstation connected. Start services first.",
430
452
  "services.title": "Services",
431
453
  "services.start": "Start services",
432
454
  "services.stop": "Stop services",
@@ -436,71 +458,74 @@ var require_en = __commonJS({
436
458
  "services.restarted": "Services restarted.",
437
459
  "services.startFailed": "Could not start services: {message}",
438
460
  "services.stopFailed": "Could not stop services: {message}",
439
- "services.unsavedBlocked": "There are unsaved changes. Save them first, or the services restart on the configuration still on disk.",
440
- "services.managedNotice": "These services are managed by {manager}; the keep-alive unit was used to apply the change.",
441
- "services.logs": "Recent log output",
442
- "services.logEmpty": "No log output yet.",
461
+ "services.unsavedBlocked": "Unsaved changes. Save before restarting.",
462
+ "services.managedNotice": "Managed by {manager}; applied via keep-alive service.",
463
+ "services.logs": "Recent logs",
464
+ "services.logEmpty": "No logs.",
443
465
  "services.logRelay": "Relay",
444
466
  "services.logHost": "Host connector",
445
467
  "relay.title": "Relay settings",
446
468
  "relay.settings": "Settings",
469
+ "relay.locked": "fixed",
470
+ "relay.officialFixed": "The official relay address is fixed and cannot be edited.",
471
+ "relay.officialHint": "Official relay: address is fixed and no password is needed.",
447
472
  "relay.password": "Relay password",
448
- "relay.passwordHint": "Must match RELAY_PASSWORD on the relay. Leave empty for a public relay.",
473
+ "relay.passwordHint": "Must match RELAY_PASSWORD on relay. Leave empty if public.",
449
474
  "relay.passwordSet": "set",
450
- "relay.passwordEmpty": "not set (public relay)",
451
- "relay.passwordSaved": "Relay password saved. Restart the services to apply it.",
452
- "relay.showPassword": "Show the password",
453
- "relay.hidePassword": "Hide the password",
454
- "relay.identity": "Workstation id",
455
- "relay.regenerate": "Generate a new workstation identity",
456
- "relay.regenerated": "New workstation identity generated. Restart the services to enrol again.",
457
- "relay.envSnippet": "Command for your relay server",
458
- "relay.envHint": "Run this on the relay host.",
459
- "relay.test": "Test the relay connection",
475
+ "relay.passwordEmpty": "not set (public)",
476
+ "relay.passwordSaved": "Relay password saved. Restart services to apply.",
477
+ "relay.showPassword": "Show password",
478
+ "relay.hidePassword": "Hide password",
479
+ "relay.identity": "Workstation ID",
480
+ "relay.regenerate": "Generate new ID",
481
+ "relay.regenerated": "New workstation identity generated. Restart services to re-register.",
482
+ "relay.envSnippet": "Relay startup command",
483
+ "relay.envHint": "Run this command on the relay host.",
484
+ "relay.test": "Test relay connection",
460
485
  "relay.testOk": "Relay reachable: version {version}, {hosts} workstation(s) connected.",
461
486
  "relay.testFailed": "Relay unreachable: {message}",
462
- "relay.selectAddress": "Choose the address to advertise",
487
+ "relay.selectAddress": "Choose advertised address",
463
488
  "relay.listenAddress": "Listen address",
464
- "relay.listenAddressHint": "This is the server bind address. The advertised address below is what browsers open.",
489
+ "relay.listenAddressHint": "Server bind address. Browsers use advertised address below.",
465
490
  "relay.addressTailscale": "Tailscale",
466
491
  "relay.addressLan": "LAN",
467
492
  "relay.addressVirtual": "virtual",
468
493
  "relay.addressLoopback": "loopback",
469
- "relay.noAddresses": "No non-loopback addresses found. Connect to a network or start Tailscale.",
470
- "relay.docsHint": "Running your own relay: see docs/self-hosted-relay.md",
494
+ "relay.noAddresses": "No non-loopback addresses found.",
495
+ "relay.docsHint": "Self-hosting guide: docs/self-hosted-relay.md",
471
496
  "keepalive.title": "Keep-alive service",
472
497
  "keepalive.manager": "Manager",
473
498
  "keepalive.state": "State",
474
499
  "keepalive.install": "Install and start",
475
500
  "keepalive.uninstall": "Stop and remove",
476
- "keepalive.restart": "Restart the service",
501
+ "keepalive.restart": "Restart service",
477
502
  "keepalive.installed": "Keep-alive installed with {manager}.",
478
503
  "keepalive.uninstalled": "Keep-alive removed.",
479
504
  "keepalive.restarted": "Keep-alive service restarted.",
480
505
  "keepalive.failed": "Keep-alive operation failed: {message}",
481
506
  "keepalive.unitPath": "Unit file",
482
- "keepalive.logsHint": "Follow logs with: {command}",
483
- "keepalive.linger": "Start at boot without logging in",
484
- "keepalive.lingerEnabled": "Lingering is enabled: the service starts at boot.",
485
- "keepalive.lingerDisabled": "Lingering is off, so the service only runs while you are logged in.",
507
+ "keepalive.logsHint": "View logs: {command}",
508
+ "keepalive.linger": "Start at boot",
509
+ "keepalive.lingerEnabled": "Start at boot enabled.",
510
+ "keepalive.lingerDisabled": "Start at boot disabled (runs while logged in).",
486
511
  "keepalive.enableLinger": "Enable start at boot",
487
- "keepalive.lingerDone": "Lingering enabled for {username}.",
488
- "keepalive.fallbackNote": "No system service manager is available; a supervised background process is used instead. It does not survive a reboot.",
512
+ "keepalive.lingerDone": "Start at boot enabled for {username}.",
513
+ "keepalive.fallbackNote": "No system service manager found; background process will not survive reboot.",
489
514
  "herdr.title": "Herdr integration",
490
515
  "herdr.socketPath": "Socket path",
491
516
  "herdr.args": "Extra arguments",
492
517
  "herdr.plugin": "Plugin registration",
493
518
  "herdr.pluginRegistered": "Registered with Herdr.",
494
519
  "herdr.pluginMissing": "Not registered with Herdr.",
495
- "herdr.register": "Register this package as a Herdr plugin",
496
- "herdr.unregister": "Unregister the Herdr plugin",
520
+ "herdr.register": "Register as Herdr plugin",
521
+ "herdr.unregister": "Unregister plugin",
497
522
  "herdr.registerDone": "Registered: {path}",
498
523
  "herdr.unregisterDone": "Plugin unregistered.",
499
524
  "herdr.registerFailed": "Registration failed: {message}",
500
- "herdr.cliMissing": "The herdr command was not found on PATH.",
525
+ "herdr.cliMissing": "Command herdr not found in PATH.",
501
526
  "about.title": "Language & about",
502
527
  "about.language": "Interface language",
503
- "about.languageAuto": "Follow the system ({detected})",
528
+ "about.languageAuto": "Follow system ({detected})",
504
529
  "about.languageZh": "\u4E2D\u6587",
505
530
  "about.languageEn": "English",
506
531
  "about.version": "Version",
@@ -510,20 +535,20 @@ var require_en = __commonJS({
510
535
  "about.docs": "Self-hosting guide: docs/self-hosted-relay.md",
511
536
  "wizard.title": "First-time setup",
512
537
  "wizard.step": "Step {current} of {total}",
513
- "wizard.languageTitle": "Choose your language",
514
- "wizard.accessTitle": "How do you want to reach this machine?",
515
- "wizard.accessHint": "You can change this later on the Relay screen.",
516
- "wizard.addressTitle": "Which address should phones use?",
517
- "wizard.relayTitle": "Your relay server",
538
+ "wizard.languageTitle": "Choose language",
539
+ "wizard.accessTitle": "Access mode",
540
+ "wizard.accessHint": "Can be changed later in Relay settings.",
541
+ "wizard.addressTitle": "Browser access address",
542
+ "wizard.relayTitle": "Relay server",
518
543
  "wizard.relayUrlLabel": "Relay URL (wss://\u2026)",
519
- "wizard.relayHint": "The relay must already be running. See docs/self-hosted-relay.md to set one up.",
544
+ "wizard.relayHint": "Relay must be running. See docs/self-hosted-relay.md.",
520
545
  "wizard.passwordTitle": "Relay password",
521
- "wizard.passwordHint": "The RELAY_PASSWORD your relay was started with. Leave it empty if the relay has none.",
522
- "wizard.finishTitle": "Ready",
546
+ "wizard.passwordHint": "RELAY_PASSWORD of the relay. Leave empty if unset.",
547
+ "wizard.finishTitle": "Finish",
523
548
  "wizard.finishHint": "Configuration will be saved to {path}.",
524
- "wizard.startNow": "Start the services now",
525
- "wizard.installKeepalive": "Keep the services running in the background",
526
- "wizard.finish": "Save and continue",
549
+ "wizard.startNow": "Start services now",
550
+ "wizard.installKeepalive": "Run in background",
551
+ "wizard.finish": "Save and finish",
527
552
  "field.mode": "Access mode",
528
553
  "field.port": "Relay port",
529
554
  "field.lanHost": "Browser address",
@@ -536,15 +561,15 @@ var require_en = __commonJS({
536
561
  "placeholder.autoDiscovered": "auto-discovered",
537
562
  "placeholder.none": "none",
538
563
  "error.invalidMode": "Unknown access mode.",
539
- "error.invalidPort": "The port must be a number between 1 and 65535.",
540
- "error.invalidLanHost": "The browser address must be a reachable LAN or Tailscale address, not loopback or 0.0.0.0.",
541
- "error.invalidRelayUrl": "The relay URL must start with wss://, ws://, https:// or http://.",
542
- "error.invalidPublicUrl": "The browser URL must start with https:// or http:// and cannot use 0.0.0.0 or ::.",
564
+ "error.invalidPort": "Port must be a number between 1 and 65535.",
565
+ "error.invalidLanHost": "Browser address must be a reachable LAN or Tailscale address, not loopback or 0.0.0.0.",
566
+ "error.invalidRelayUrl": "Relay URL must start with wss://, ws://, https:// or http://.",
567
+ "error.invalidPublicUrl": "Browser URL must start with https:// or http:// and cannot use 0.0.0.0 or ::.",
543
568
  "error.invalidLanguage": "Unknown language.",
544
569
  "error.invalidKeepalive": "Unknown keep-alive manager.",
545
570
  "error.unknownField": "Unknown setting.",
546
- "error.remoteUrlRequired": "A relay URL is required for the self-hosted relay mode.",
547
- "error.saveFailed": "Could not save the configuration: {message}",
571
+ "error.remoteUrlRequired": "Relay URL is required for self-hosted relay mode.",
572
+ "error.saveFailed": "Could not save configuration: {message}",
548
573
  "hint.navigate": "\u2191\u2193 move",
549
574
  "hint.select": "\u21B5 select",
550
575
  "hint.edit": "\u21B5 edit",
@@ -554,22 +579,23 @@ var require_en = __commonJS({
554
579
  "hint.mouseOn": "m mouse off",
555
580
  "hint.mouseOff": "m mouse on",
556
581
  "hint.mouseUnsupported": "mouse unsupported",
557
- "hint.restartRequired": "Restart the services to apply these changes.",
558
- "hint.unsavedChanges": "Unsaved changes \u2014 press s to save.",
582
+ "hint.restartRequired": "Restart services to apply changes.",
583
+ "hint.unsavedChanges": "Unsaved changes (press s to save).",
559
584
  "hint.save": "s save",
560
585
  "hint.editing": "\u21B5 confirm \xB7 esc cancel",
561
586
  "update.title": "Updates",
562
587
  "update.check": "Check for updates",
563
588
  "update.checking": "Checking npm\u2026",
564
589
  "update.upToDate": "Up to date ({version})",
565
- "update.available": "Update available: {version} \u2014 press Enter to install",
590
+ "update.available": "Update {version} available (press Enter to install)",
566
591
  "update.updating": "Installing {version}\u2026",
567
592
  "update.done": "Updated to {version}",
568
- "update.restartHint": "Update installed. Restart herdr-remote to use it.",
569
- "update.errorNetwork": "Could not reach the npm registry",
570
- "update.errorFailed": "The update failed. Try: npm install -g herdr-remote@latest",
571
- "update.cannot.source": "Running from a source checkout \u2014 update with git, not npm",
572
- "update.cannot.linked": "Running from a linked working copy \u2014 npm link manages this one"
593
+ "update.restartHint": "Update installed. Restart herdr-remote to apply.",
594
+ "update.errorNetwork": "Could not reach any npm registry",
595
+ "update.errorNetworkDetail": "No registry answered: {message}",
596
+ "update.errorFailed": "Update failed. Run: npm install -g herdr-remote@latest",
597
+ "update.cannot.source": "Source checkout \u2014 update with git",
598
+ "update.cannot.linked": "Linked package \u2014 managed by npm link"
573
599
  };
574
600
  }
575
601
  });
@@ -580,7 +606,7 @@ var require_zh = __commonJS({
580
606
  "use strict";
581
607
  module.exports = {
582
608
  "app.name": "Herdr Remote",
583
- "app.tagline": "\u5728\u6D4F\u89C8\u5668\u91CC\u8FDC\u7A0B\u4F7F\u7528\u4F60\u7684 Herdr \u5DE5\u4F5C\u533A",
609
+ "app.tagline": "\u5728\u6D4F\u89C8\u5668\u4E2D\u4F7F\u7528 Herdr \u5DE5\u4F5C\u533A",
584
610
  "common.yes": "\u662F",
585
611
  "common.no": "\u5426",
586
612
  "common.on": "\u5F00",
@@ -598,11 +624,11 @@ var require_zh = __commonJS({
598
624
  "common.disabled": "\u5DF2\u505C\u7528",
599
625
  "common.loading": "\u5904\u7406\u4E2D\u2026",
600
626
  "common.save": "\u4FDD\u5B58\u8BBE\u7F6E",
601
- "common.saved": "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 {path}",
627
+ "common.saved": "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 {path}",
602
628
  "common.back": "\u8FD4\u56DE",
603
629
  "common.cancel": "\u53D6\u6D88",
604
630
  "common.confirm": "\u786E\u8BA4",
605
- "common.pid": "\u8FDB\u7A0B {pid}",
631
+ "common.pid": "PID {pid}",
606
632
  "common.notApplicable": "\u4E0D\u9002\u7528",
607
633
  "nav.overview": "\u6982\u89C8",
608
634
  "nav.pair": "\u914D\u5BF9\u8BBE\u5907",
@@ -613,12 +639,12 @@ var require_zh = __commonJS({
613
639
  "nav.about": "\u8BED\u8A00\u4E0E\u5173\u4E8E",
614
640
  "mode.local": "\u4EC5\u672C\u673A",
615
641
  "mode.lan": "\u5C40\u57DF\u7F51 / Tailscale",
616
- "mode.official": "\u5B98\u65B9 relay\uFF08\u65E0\u9700\u81EA\u5EFA\u670D\u52A1\u5668\uFF09",
617
- "mode.remote": "\u81EA\u5EFA relay",
618
- "mode.local.description": "relay \u76D1\u542C 127.0.0.1\uFF0C\u53EA\u6709\u672C\u673A\u6D4F\u89C8\u5668\u80FD\u8BBF\u95EE\u3002",
619
- "mode.lan.description": "relay \u76D1\u542C 0.0.0.0\uFF08\u6240\u6709\u7F51\u5361\uFF09\uFF0C\u5C40\u57DF\u7F51\u6216 tailnet \u91CC\u7684\u624B\u673A\u90FD\u80FD\u8BBF\u95EE\u3002",
620
- "mode.official.description": "\u901A\u8FC7 Herdr Remote \u5B98\u65B9 relay \u4ECE\u4EFB\u610F\u7F51\u7EDC\u8BBF\u95EE\u672C\u673A\uFF0C\u65E0\u9700\u90E8\u7F72\u4EFB\u4F55\u670D\u52A1\u5668\u3002\u7EC8\u7AEF\u6D41\u91CF\u4F1A\u7ECF\u8FC7\u4E00\u53F0\u4F60\u65E0\u6CD5\u63A7\u5236\u7684\u670D\u52A1\u5668\uFF0C\u4F46\u53EA\u6709\u4F60\u914D\u5BF9\u8FC7\u7684\u8BBE\u5907\u624D\u80FD\u6253\u5F00\u7EC8\u7AEF\u3002",
621
- "mode.remote.description": "\u4E0D\u542F\u52A8\u672C\u5730 relay\u3002\u672C\u673A\u8FDE\u63A5\u4F60\u81EA\u5EFA\u7684 relay\uFF0C\u8FD9\u662F\u4ECE\u5916\u7F51\u8BBF\u95EE\u7684\u552F\u4E00\u65B9\u5F0F\u3002",
642
+ "mode.official": "\u5B98\u65B9 Relay",
643
+ "mode.remote": "\u81EA\u5EFA Relay",
644
+ "mode.local.description": "\u76D1\u542C 127.0.0.1\uFF0C\u4EC5\u672C\u673A\u53EF\u8BBF\u95EE\u3002",
645
+ "mode.lan.description": "\u76D1\u542C 0.0.0.0\uFF0C\u5C40\u57DF\u7F51\u4E0E Tailscale \u8BBE\u5907\u53EF\u8BBF\u95EE\u3002",
646
+ "mode.official.description": "\u4F7F\u7528\u5B98\u65B9 Relay \u8DE8\u7F51\u8BBF\u95EE\uFF0C\u65E0\u9700\u81EA\u5EFA\u670D\u52A1\u5668\u3002",
647
+ "mode.remote.description": "\u8FDE\u63A5\u81EA\u5EFA Relay \u8DE8\u7F51\u8BBF\u95EE\u3002",
622
648
  "overview.title": "\u72B6\u6001",
623
649
  "overview.mode": "\u8BBF\u95EE\u65B9\u5F0F",
624
650
  "overview.relay": "Relay",
@@ -628,25 +654,25 @@ var require_zh = __commonJS({
628
654
  "overview.keepalive": "\u4FDD\u6D3B\u670D\u52A1",
629
655
  "overview.devices": "\u6D4F\u89C8\u5668",
630
656
  "overview.hosts": "\u5DE5\u4F5C\u7AD9",
631
- "overview.uptime": "Relay \u8FD0\u884C\u65F6\u957F",
657
+ "overview.uptime": "Relay \u8FD0\u884C\u65F6\u95F4",
632
658
  "overview.relayLocal": "\u672C\u5730\uFF0C{bind}:{port}",
633
659
  "overview.relayRemote": "\u8FDC\u7A0B\uFF0C{url}",
634
- "overview.socketMissing": "\u672A\u627E\u5230 \u2014 Herdr \u5728\u8FD0\u884C\u5417\uFF1F",
660
+ "overview.socketMissing": "\u672A\u627E\u5230\uFF0C\u8BF7\u786E\u8BA4 Herdr \u662F\u5426\u8FD0\u884C",
635
661
  "overview.unreachable": "\u65E0\u6CD5\u8BBF\u95EE\uFF1A{message}",
636
- "overview.notStarted": "\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u8BF7\u5230\u300C\u670D\u52A1\u300D\u9875\u9762\u542F\u52A8\u3002",
662
+ "overview.notStarted": "\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u53EF\u524D\u5F80\u300C\u670D\u52A1\u300D\u542F\u52A8\u3002",
637
663
  "pair.title": "\u914D\u5BF9\u8BBE\u5907",
638
- "pair.generate": "\u751F\u6210\u4E00\u6B21\u6027\u914D\u5BF9\u7801",
639
- "pair.regenerate": "\u91CD\u65B0\u751F\u6210\u914D\u5BF9\u7801",
640
- "pair.working": "\u6B63\u5728\u542F\u52A8\u670D\u52A1\u5E76\u7533\u8BF7\u914D\u5BF9\u7801\u2026",
664
+ "pair.generate": "\u751F\u6210\u914D\u5BF9\u7801",
665
+ "pair.regenerate": "\u91CD\u65B0\u751F\u6210",
666
+ "pair.working": "\u751F\u6210\u914D\u5BF9\u7801\u4E2D\u2026",
641
667
  "pair.code": "\u914D\u5BF9\u7801",
642
- "pair.url": "\u5728\u624B\u673A\u4E0A\u6253\u5F00",
668
+ "pair.url": "\u8BBF\u95EE\u5730\u5740",
643
669
  "pair.expires": "\u5269\u4F59 {minutes}\uFF08\u81F3 {time}\uFF09",
644
- "pair.expired": "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002",
645
- "pair.instructions": "\u6253\u5F00\u94FE\u63A5\u3001\u8F93\u5165\u914D\u5BF9\u7801\uFF0C\u4E4B\u540E\u8FD9\u53F0\u6D4F\u89C8\u5668\u5C31\u957F\u671F\u53EF\u7528\u4E86\u3002",
646
- "pair.qrHint": "\u7528\u624B\u673A\u76F8\u673A\u626B\u7801\u5373\u53EF\u6253\u5F00\u94FE\u63A5\u3002",
647
- "pair.qrUnavailable": "\u7EC8\u7AEF\u5BBD\u5EA6\u4E0D\u8DB3\u4EE5\u663E\u793A\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u4F7F\u7528\u4E0A\u9762\u7684\u94FE\u63A5\u3002",
670
+ "pair.expired": "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u9700\u91CD\u65B0\u751F\u6210\u3002",
671
+ "pair.instructions": "\u6253\u5F00\u94FE\u63A5\u5E76\u8F93\u5165\u914D\u5BF9\u7801\u3002",
672
+ "pair.qrHint": "\u626B\u63CF\u4E8C\u7EF4\u7801\u6253\u5F00\u94FE\u63A5\u3002",
673
+ "pair.qrUnavailable": "\u7EC8\u7AEF\u5BBD\u5EA6\u4E0D\u8DB3\uFF0C\u65E0\u6CD5\u663E\u793A\u4E8C\u7EF4\u7801\u3002",
648
674
  "pair.failed": "\u751F\u6210\u914D\u5BF9\u7801\u5931\u8D25\uFF1A{message}",
649
- "pair.hostOffline": "relay \u4E0A\u8FD8\u6CA1\u6709\u5DE5\u4F5C\u7AD9\u8FDE\u63A5\uFF0C\u8BF7\u5148\u542F\u52A8\u670D\u52A1\u3002",
675
+ "pair.hostOffline": "Relay \u672A\u8FDE\u63A5\u5DE5\u4F5C\u7AD9\uFF0C\u9700\u5148\u542F\u52A8\u670D\u52A1\u3002",
650
676
  "services.title": "\u670D\u52A1",
651
677
  "services.start": "\u542F\u52A8\u670D\u52A1",
652
678
  "services.stop": "\u505C\u6B62\u670D\u52A1",
@@ -656,38 +682,41 @@ var require_zh = __commonJS({
656
682
  "services.restarted": "\u670D\u52A1\u5DF2\u91CD\u542F\u3002",
657
683
  "services.startFailed": "\u542F\u52A8\u670D\u52A1\u5931\u8D25\uFF1A{message}",
658
684
  "services.stopFailed": "\u505C\u6B62\u670D\u52A1\u5931\u8D25\uFF1A{message}",
659
- "services.unsavedBlocked": "\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\u3002\u8BF7\u5148\u4FDD\u5B58\uFF0C\u5426\u5219\u670D\u52A1\u4F1A\u7528\u78C1\u76D8\u4E0A\u7684\u65E7\u914D\u7F6E\u91CD\u542F\u3002",
660
- "services.managedNotice": "\u670D\u52A1\u7531 {manager} \u6258\u7BA1\uFF0C\u5DF2\u901A\u8FC7\u4FDD\u6D3B\u670D\u52A1\u6267\u884C\u6B64\u64CD\u4F5C\u3002",
685
+ "services.unsavedBlocked": "\u914D\u7F6E\u672A\u4FDD\u5B58\uFF0C\u8BF7\u5148\u4FDD\u5B58\u518D\u91CD\u542F\u3002",
686
+ "services.managedNotice": "\u7531 {manager} \u6258\u7BA1\uFF0C\u5DF2\u901A\u8FC7\u4FDD\u6D3B\u670D\u52A1\u6267\u884C\u3002",
661
687
  "services.logs": "\u6700\u8FD1\u65E5\u5FD7",
662
- "services.logEmpty": "\u6682\u65E0\u65E5\u5FD7\u8F93\u51FA\u3002",
688
+ "services.logEmpty": "\u6682\u65E0\u65E5\u5FD7\u3002",
663
689
  "services.logRelay": "Relay",
664
690
  "services.logHost": "\u4E3B\u673A\u8FDE\u63A5\u5668",
665
691
  "relay.title": "Relay \u8BBE\u7F6E",
666
692
  "relay.settings": "\u8BBE\u7F6E",
693
+ "relay.locked": "\u56FA\u5B9A",
694
+ "relay.officialFixed": "\u5B98\u65B9 relay \u5730\u5740\u56FA\u5B9A\uFF0C\u4E0D\u53EF\u4FEE\u6539\u3002",
695
+ "relay.officialHint": "\u5B98\u65B9 relay\uFF1A\u5730\u5740\u56FA\u5B9A\uFF0C\u65E0\u9700\u5BC6\u7801\u3002",
667
696
  "relay.password": "Relay \u5BC6\u7801",
668
- "relay.passwordHint": "\u9700\u4E0E relay \u4E0A\u7684 RELAY_PASSWORD \u4E00\u81F4\u3002\u7559\u7A7A\u8868\u793A\u516C\u7528 relay\u3002",
697
+ "relay.passwordHint": "\u987B\u4E0E Relay \u7AEF\u7684 RELAY_PASSWORD \u4E00\u81F4\u3002\u7559\u7A7A\u4E3A\u516C\u7528 Relay\u3002",
669
698
  "relay.passwordSet": "\u5DF2\u8BBE\u7F6E",
670
- "relay.passwordEmpty": "\u672A\u8BBE\u7F6E\uFF08\u516C\u7528 relay\uFF09",
671
- "relay.passwordSaved": "Relay \u5BC6\u7801\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\u3002",
699
+ "relay.passwordEmpty": "\u672A\u8BBE\u7F6E\uFF08\u516C\u7528\uFF09",
700
+ "relay.passwordSaved": "Relay \u5BC6\u7801\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F\u670D\u52A1\u751F\u6548\u3002",
672
701
  "relay.showPassword": "\u663E\u793A\u5BC6\u7801",
673
702
  "relay.hidePassword": "\u9690\u85CF\u5BC6\u7801",
674
703
  "relay.identity": "\u5DE5\u4F5C\u7AD9\u6807\u8BC6",
675
- "relay.regenerate": "\u91CD\u65B0\u751F\u6210\u5DE5\u4F5C\u7AD9\u6807\u8BC6",
676
- "relay.regenerated": "\u5DF2\u751F\u6210\u65B0\u7684\u5DE5\u4F5C\u7AD9\u6807\u8BC6\uFF0C\u91CD\u542F\u670D\u52A1\u540E\u4F1A\u91CD\u65B0\u6CE8\u518C\u3002",
677
- "relay.envSnippet": "relay \u670D\u52A1\u5668\u542F\u52A8\u547D\u4EE4",
678
- "relay.envHint": "\u5728 relay \u4E3B\u673A\u4E0A\u6267\u884C\u8FD9\u6761\u547D\u4EE4\u3002",
679
- "relay.test": "\u6D4B\u8BD5 relay \u8FDE\u63A5",
680
- "relay.testOk": "relay \u53EF\u8BBF\u95EE\uFF1A\u7248\u672C {version}\uFF0C\u5DF2\u8FDE\u63A5 {hosts} \u53F0\u5DE5\u4F5C\u7AD9\u3002",
681
- "relay.testFailed": "relay \u65E0\u6CD5\u8BBF\u95EE\uFF1A{message}",
682
- "relay.selectAddress": "\u9009\u62E9\u5BF9\u5916\u516C\u5E03\u7684\u5730\u5740",
704
+ "relay.regenerate": "\u91CD\u65B0\u751F\u6210\u6807\u8BC6",
705
+ "relay.regenerated": "\u5DF2\u751F\u6210\u65B0\u6807\u8BC6\uFF0C\u91CD\u542F\u540E\u91CD\u65B0\u6CE8\u518C\u3002",
706
+ "relay.envSnippet": "Relay \u542F\u52A8\u547D\u4EE4",
707
+ "relay.envHint": "\u5728 Relay \u4E3B\u673A\u6267\u884C\u6B64\u547D\u4EE4\u3002",
708
+ "relay.test": "\u6D4B\u8BD5 Relay \u8FDE\u63A5",
709
+ "relay.testOk": "Relay \u53EF\u8BBF\u95EE\uFF1A\u7248\u672C {version}\uFF0C\u5DF2\u8FDE\u63A5 {hosts} \u53F0\u5DE5\u4F5C\u7AD9\u3002",
710
+ "relay.testFailed": "Relay \u65E0\u6CD5\u8BBF\u95EE\uFF1A{message}",
711
+ "relay.selectAddress": "\u9009\u62E9\u516C\u5E03\u5730\u5740",
683
712
  "relay.listenAddress": "\u76D1\u542C\u5730\u5740",
684
- "relay.listenAddressHint": "\u8FD9\u662F\u670D\u52A1\u5668\u7ED1\u5B9A\u5730\u5740\uFF1B\u4E0B\u9762\u7684\u5BF9\u5916\u516C\u5E03\u5730\u5740\u624D\u662F\u6D4F\u89C8\u5668\u8981\u6253\u5F00\u7684\u5730\u5740\u3002",
713
+ "relay.listenAddressHint": "\u670D\u52A1\u5668\u7ED1\u5B9A\u5730\u5740\uFF1B\u6D4F\u89C8\u5668\u8BBF\u95EE\u4E0B\u65B9\u516C\u5E03\u5730\u5740\u3002",
685
714
  "relay.addressTailscale": "Tailscale",
686
715
  "relay.addressLan": "\u5C40\u57DF\u7F51",
687
716
  "relay.addressVirtual": "\u865A\u62DF\u7F51\u5361",
688
717
  "relay.addressLoopback": "\u56DE\u73AF",
689
- "relay.noAddresses": "\u6CA1\u6709\u627E\u5230\u975E\u56DE\u73AF\u5730\u5740\uFF0C\u8BF7\u5148\u8FDE\u63A5\u7F51\u7EDC\u6216\u542F\u52A8 Tailscale\u3002",
690
- "relay.docsHint": "\u81EA\u5EFA relay \u8BF7\u53C2\u9605 docs/self-hosted-relay.zh-CN.md",
718
+ "relay.noAddresses": "\u672A\u627E\u5230\u975E\u56DE\u73AF\u5730\u5740\u3002",
719
+ "relay.docsHint": "\u81EA\u5EFA\u6307\u5357\uFF1Adocs/self-hosted-relay.zh-CN.md",
691
720
  "keepalive.title": "\u540E\u53F0\u4FDD\u6D3B\u670D\u52A1",
692
721
  "keepalive.manager": "\u7BA1\u7406\u65B9\u5F0F",
693
722
  "keepalive.state": "\u72B6\u6001",
@@ -700,24 +729,24 @@ var require_zh = __commonJS({
700
729
  "keepalive.failed": "\u4FDD\u6D3B\u64CD\u4F5C\u5931\u8D25\uFF1A{message}",
701
730
  "keepalive.unitPath": "\u5355\u5143\u6587\u4EF6",
702
731
  "keepalive.logsHint": "\u67E5\u770B\u65E5\u5FD7\uFF1A{command}",
703
- "keepalive.linger": "\u672A\u767B\u5F55\u4E5F\u5F00\u673A\u81EA\u542F",
704
- "keepalive.lingerEnabled": "\u5DF2\u5F00\u542F linger\uFF1A\u5F00\u673A\u5373\u542F\u52A8\u3002",
705
- "keepalive.lingerDisabled": "\u672A\u5F00\u542F linger\uFF0C\u670D\u52A1\u53EA\u5728\u4F60\u767B\u5F55\u671F\u95F4\u8FD0\u884C\u3002",
732
+ "keepalive.linger": "\u5F00\u673A\u81EA\u542F",
733
+ "keepalive.lingerEnabled": "\u5DF2\u5F00\u542F\u5F00\u673A\u81EA\u542F\u3002",
734
+ "keepalive.lingerDisabled": "\u672A\u5F00\u542F\u5F00\u673A\u81EA\u542F\uFF08\u4EC5\u767B\u5F55\u671F\u95F4\u8FD0\u884C\uFF09\u3002",
706
735
  "keepalive.enableLinger": "\u5F00\u542F\u5F00\u673A\u81EA\u542F",
707
- "keepalive.lingerDone": "\u5DF2\u4E3A {username} \u5F00\u542F linger\u3002",
708
- "keepalive.fallbackNote": "\u7CFB\u7EDF\u6CA1\u6709\u53EF\u7528\u7684\u670D\u52A1\u7BA1\u7406\u5668\uFF0C\u6539\u7528\u5185\u7F6E\u7684\u540E\u53F0\u5B88\u62A4\u8FDB\u7A0B\uFF0C\u91CD\u542F\u7535\u8111\u540E\u4E0D\u4F1A\u81EA\u52A8\u6062\u590D\u3002",
736
+ "keepalive.lingerDone": "\u5DF2\u4E3A {username} \u5F00\u542F\u5F00\u673A\u81EA\u542F\u3002",
737
+ "keepalive.fallbackNote": "\u65E0\u7CFB\u7EDF\u670D\u52A1\u7BA1\u7406\u5668\uFF0C\u4F7F\u7528\u540E\u53F0\u5B88\u62A4\u8FDB\u7A0B\uFF0C\u91CD\u542F\u540E\u4E0D\u81EA\u52A8\u6062\u590D\u3002",
709
738
  "herdr.title": "Herdr \u96C6\u6210",
710
739
  "herdr.socketPath": "\u5957\u63A5\u5B57\u8DEF\u5F84",
711
740
  "herdr.args": "\u9644\u52A0\u53C2\u6570",
712
741
  "herdr.plugin": "\u63D2\u4EF6\u6CE8\u518C",
713
742
  "herdr.pluginRegistered": "\u5DF2\u6CE8\u518C\u5230 Herdr\u3002",
714
743
  "herdr.pluginMissing": "\u5C1A\u672A\u6CE8\u518C\u5230 Herdr\u3002",
715
- "herdr.register": "\u628A\u672C\u5305\u6CE8\u518C\u4E3A Herdr \u63D2\u4EF6",
716
- "herdr.unregister": "\u53D6\u6D88\u6CE8\u518C Herdr \u63D2\u4EF6",
744
+ "herdr.register": "\u6CE8\u518C\u4E3A Herdr \u63D2\u4EF6",
745
+ "herdr.unregister": "\u53D6\u6D88\u6CE8\u518C",
717
746
  "herdr.registerDone": "\u5DF2\u6CE8\u518C\uFF1A{path}",
718
- "herdr.unregisterDone": "\u63D2\u4EF6\u5DF2\u53D6\u6D88\u6CE8\u518C\u3002",
747
+ "herdr.unregisterDone": "\u5DF2\u53D6\u6D88\u6CE8\u518C\u3002",
719
748
  "herdr.registerFailed": "\u6CE8\u518C\u5931\u8D25\uFF1A{message}",
720
- "herdr.cliMissing": "PATH \u4E2D\u627E\u4E0D\u5230 herdr \u547D\u4EE4\u3002",
749
+ "herdr.cliMissing": "PATH \u4E2D\u672A\u627E\u5230 herdr \u547D\u4EE4\u3002",
721
750
  "about.title": "\u8BED\u8A00\u4E0E\u5173\u4E8E",
722
751
  "about.language": "\u754C\u9762\u8BED\u8A00",
723
752
  "about.languageAuto": "\u8DDF\u968F\u7CFB\u7EDF\uFF08{detected}\uFF09",
@@ -727,23 +756,23 @@ var require_zh = __commonJS({
727
756
  "about.configPath": "\u914D\u7F6E\u6587\u4EF6",
728
757
  "about.statePath": "\u72B6\u6001\u76EE\u5F55",
729
758
  "about.relayPackage": "Relay \u5305",
730
- "about.docs": "\u81EA\u5EFA relay \u6307\u5357\uFF1Adocs/self-hosted-relay.zh-CN.md",
759
+ "about.docs": "\u81EA\u5EFA\u6307\u5357\uFF1Adocs/self-hosted-relay.zh-CN.md",
731
760
  "wizard.title": "\u9996\u6B21\u8BBE\u7F6E",
732
761
  "wizard.step": "\u7B2C {current} / {total} \u6B65",
733
762
  "wizard.languageTitle": "\u9009\u62E9\u8BED\u8A00",
734
- "wizard.accessTitle": "\u4F60\u6253\u7B97\u600E\u4E48\u8BBF\u95EE\u8FD9\u53F0\u673A\u5668\uFF1F",
735
- "wizard.accessHint": "\u4E4B\u540E\u53EF\u4EE5\u5728 Relay \u9875\u9762\u968F\u65F6\u4FEE\u6539\u3002",
736
- "wizard.addressTitle": "\u624B\u673A\u5E94\u8BE5\u4F7F\u7528\u54EA\u4E2A\u5730\u5740\uFF1F",
737
- "wizard.relayTitle": "\u4F60\u7684 relay \u670D\u52A1\u5668",
763
+ "wizard.accessTitle": "\u9009\u62E9\u8BBF\u95EE\u65B9\u5F0F",
764
+ "wizard.accessHint": "\u540E\u7EED\u53EF\u5728 Relay \u9875\u9762\u4FEE\u6539\u3002",
765
+ "wizard.addressTitle": "\u9009\u62E9\u8BBF\u95EE\u5730\u5740",
766
+ "wizard.relayTitle": "Relay \u670D\u52A1\u5668",
738
767
  "wizard.relayUrlLabel": "Relay \u5730\u5740\uFF08wss://\u2026\uFF09",
739
- "wizard.relayHint": "relay \u9700\u8981\u5DF2\u7ECF\u5728\u8FD0\u884C\u3002\u642D\u5EFA\u65B9\u6CD5\u89C1 docs/self-hosted-relay.zh-CN.md\u3002",
768
+ "wizard.relayHint": "Relay \u9700\u5148\u8FD0\u884C\u3002\u642D\u5EFA\u6307\u5357\u89C1 docs/self-hosted-relay.zh-CN.md\u3002",
740
769
  "wizard.passwordTitle": "Relay \u5BC6\u7801",
741
- "wizard.passwordHint": "relay \u542F\u52A8\u65F6\u8BBE\u7F6E\u7684 RELAY_PASSWORD\u3002relay \u6CA1\u8BBE\u5BC6\u7801\u5C31\u7559\u7A7A\u3002",
770
+ "wizard.passwordHint": "Relay \u7684 RELAY_PASSWORD\u3002\u672A\u8BBE\u7559\u7A7A\u3002",
742
771
  "wizard.finishTitle": "\u5B8C\u6210",
743
- "wizard.finishHint": "\u914D\u7F6E\u5C06\u4FDD\u5B58\u5230 {path}\u3002",
772
+ "wizard.finishHint": "\u914D\u7F6E\u5C06\u4FDD\u5B58\u81F3 {path}\u3002",
744
773
  "wizard.startNow": "\u7ACB\u5373\u542F\u52A8\u670D\u52A1",
745
- "wizard.installKeepalive": "\u8BA9\u670D\u52A1\u5728\u540E\u53F0\u4FDD\u6301\u8FD0\u884C",
746
- "wizard.finish": "\u4FDD\u5B58\u5E76\u7EE7\u7EED",
774
+ "wizard.installKeepalive": "\u540E\u53F0\u4FDD\u6D3B",
775
+ "wizard.finish": "\u4FDD\u5B58\u5E76\u5B8C\u6210",
747
776
  "field.mode": "\u8BBF\u95EE\u65B9\u5F0F",
748
777
  "field.port": "Relay \u7AEF\u53E3",
749
778
  "field.lanHost": "\u6D4F\u89C8\u5668\u5730\u5740",
@@ -756,14 +785,14 @@ var require_zh = __commonJS({
756
785
  "placeholder.autoDiscovered": "\u81EA\u52A8\u63A2\u6D4B",
757
786
  "placeholder.none": "\u65E0",
758
787
  "error.invalidMode": "\u672A\u77E5\u7684\u8BBF\u95EE\u65B9\u5F0F\u3002",
759
- "error.invalidPort": "\u7AEF\u53E3\u5FC5\u987B\u662F 1 \u5230 65535 \u4E4B\u95F4\u7684\u6570\u5B57\u3002",
760
- "error.invalidLanHost": "\u6D4F\u89C8\u5668\u5730\u5740\u5FC5\u987B\u662F\u53EF\u8BBF\u95EE\u7684\u5C40\u57DF\u7F51\u6216 Tailscale \u5730\u5740\uFF0C\u4E0D\u80FD\u586B\u5199\u56DE\u73AF\u5730\u5740\u6216 0.0.0.0\u3002",
761
- "error.invalidRelayUrl": "Relay \u5730\u5740\u9700\u4EE5 wss://\u3001ws://\u3001https:// \u6216 http:// \u5F00\u5934\u3002",
762
- "error.invalidPublicUrl": "\u6D4F\u89C8\u5668\u5730\u5740\u9700\u4EE5 https:// \u6216 http:// \u5F00\u5934\uFF0C\u4E0D\u80FD\u4F7F\u7528 0.0.0.0 \u6216 ::\u3002",
788
+ "error.invalidPort": "\u7AEF\u53E3\u987B\u4E3A 1 \u5230 65535 \u4E4B\u95F4\u7684\u6570\u5B57\u3002",
789
+ "error.invalidLanHost": "\u6D4F\u89C8\u5668\u5730\u5740\u987B\u4E3A\u53EF\u8BBF\u95EE\u5C40\u57DF\u7F51\u6216 Tailscale \u5730\u5740\uFF0C\u4E0D\u80FD\u4E3A\u56DE\u73AF\u6216 0.0.0.0\u3002",
790
+ "error.invalidRelayUrl": "Relay \u5730\u5740\u987B\u4EE5 wss://\u3001ws://\u3001https:// \u6216 http:// \u5F00\u5934\u3002",
791
+ "error.invalidPublicUrl": "\u6D4F\u89C8\u5668\u5730\u5740\u987B\u4EE5 https:// \u6216 http:// \u5F00\u5934\uFF0C\u4E0D\u80FD\u4E3A 0.0.0.0 \u6216 ::\u3002",
763
792
  "error.invalidLanguage": "\u672A\u77E5\u7684\u8BED\u8A00\u3002",
764
793
  "error.invalidKeepalive": "\u672A\u77E5\u7684\u4FDD\u6D3B\u7BA1\u7406\u65B9\u5F0F\u3002",
765
794
  "error.unknownField": "\u672A\u77E5\u7684\u8BBE\u7F6E\u9879\u3002",
766
- "error.remoteUrlRequired": "\u81EA\u5EFA relay \u6A21\u5F0F\u5FC5\u987B\u586B\u5199 Relay \u5730\u5740\u3002",
795
+ "error.remoteUrlRequired": "\u81EA\u5EFA Relay \u6A21\u5F0F\u987B\u586B\u5199 Relay \u5730\u5740\u3002",
767
796
  "error.saveFailed": "\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25\uFF1A{message}",
768
797
  "hint.navigate": "\u2191\u2193 \u79FB\u52A8",
769
798
  "hint.select": "\u21B5 \u9009\u62E9",
@@ -774,22 +803,23 @@ var require_zh = __commonJS({
774
803
  "hint.mouseOn": "m \u5173\u95ED\u9F20\u6807",
775
804
  "hint.mouseOff": "m \u5F00\u542F\u9F20\u6807",
776
805
  "hint.mouseUnsupported": "\u4E0D\u652F\u6301\u9F20\u6807",
777
- "hint.restartRequired": "\u9700\u8981\u91CD\u542F\u670D\u52A1\u624D\u80FD\u751F\u6548\u3002",
778
- "hint.unsavedChanges": "\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539 \u2014 \u6309 s \u4FDD\u5B58\u3002",
806
+ "hint.restartRequired": "\u91CD\u542F\u670D\u52A1\u751F\u6548\u3002",
807
+ "hint.unsavedChanges": "\u6709\u672A\u4FDD\u5B58\u4FEE\u6539\uFF08\u6309 s \u4FDD\u5B58\uFF09\u3002",
779
808
  "hint.save": "s \u4FDD\u5B58",
780
809
  "hint.editing": "\u21B5 \u786E\u8BA4 \xB7 esc \u53D6\u6D88",
781
810
  "update.title": "\u66F4\u65B0",
782
811
  "update.check": "\u68C0\u67E5\u66F4\u65B0",
783
- "update.checking": "\u6B63\u5728\u67E5\u8BE2 npm\u2026",
812
+ "update.checking": "\u67E5\u8BE2 npm \u4E2D\u2026",
784
813
  "update.upToDate": "\u5DF2\u662F\u6700\u65B0\uFF08{version}\uFF09",
785
- "update.available": "\u6709\u65B0\u7248\u672C {version} \u2014\u2014 \u6309 Enter \u5B89\u88C5",
786
- "update.updating": "\u6B63\u5728\u5B89\u88C5 {version}\u2026",
787
- "update.done": "\u5DF2\u66F4\u65B0\u5230 {version}",
788
- "update.restartHint": "\u66F4\u65B0\u5DF2\u5B89\u88C5\uFF0C\u91CD\u542F herdr-remote \u540E\u751F\u6548\u3002",
789
- "update.errorNetwork": "\u65E0\u6CD5\u8FDE\u63A5 npm registry",
814
+ "update.available": "\u65B0\u7248\u672C {version} \u53EF\u7528\uFF08\u6309 Enter \u5B89\u88C5\uFF09",
815
+ "update.updating": "\u5B89\u88C5 {version} \u4E2D\u2026",
816
+ "update.done": "\u5DF2\u66F4\u65B0\u81F3 {version}",
817
+ "update.restartHint": "\u66F4\u65B0\u5DF2\u5B89\u88C5\uFF0C\u91CD\u542F herdr-remote \u751F\u6548\u3002",
818
+ "update.errorNetwork": "\u6240\u6709 npm registry \u5747\u65E0\u6CD5\u8FDE\u63A5",
819
+ "update.errorNetworkDetail": "\u65E0 registry \u5E94\u7B54\uFF1A{message}",
790
820
  "update.errorFailed": "\u66F4\u65B0\u5931\u8D25\uFF0C\u53EF\u624B\u52A8\u6267\u884C\uFF1Anpm install -g herdr-remote@latest",
791
- "update.cannot.source": "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C \u2014\u2014 \u8BF7\u7528 git \u66F4\u65B0\uFF0C\u800C\u4E0D\u662F npm",
792
- "update.cannot.linked": "\u5F53\u524D\u662F npm link \u7684\u5DE5\u4F5C\u526F\u672C \u2014\u2014 \u7531 npm link \u7BA1\u7406"
821
+ "update.cannot.source": "\u6E90\u7801\u76EE\u5F55\u8FD0\u884C\uFF0C\u8BF7\u4F7F\u7528 git \u66F4\u65B0",
822
+ "update.cannot.linked": "npm link \u5DE5\u4F5C\u526F\u672C\uFF0C\u7531 npm link \u7BA1\u7406"
793
823
  };
794
824
  }
795
825
  });
@@ -954,6 +984,7 @@ var require_settings_model = __commonJS({
954
984
  ACCESS_MODES: ACCESS_MODES2,
955
985
  KEEPALIVE_MANAGERS,
956
986
  LANGUAGES,
987
+ OFFICIAL_RELAY_URL: OFFICIAL_RELAY_URL2,
957
988
  configDir,
958
989
  configPath: configPath2,
959
990
  isLoopbackHost,
@@ -976,6 +1007,14 @@ var require_settings_model = __commonJS({
976
1007
  { id: "keepaliveManager", kind: "choice", choices: KEEPALIVE_MANAGERS, labelKey: "field.keepalive" }
977
1008
  ];
978
1009
  var EMPTY = "";
1010
+ var SELECTABLE_MODES2 = ["local", "lan", "official", "remote"];
1011
+ function selectedMode2(draft) {
1012
+ if (draft.relay.mode === "remote" && draft.relay.remoteUrl === OFFICIAL_RELAY_URL2) return "official";
1013
+ return draft.relay.mode;
1014
+ }
1015
+ function isOfficialRelay2(draft) {
1016
+ return selectedMode2(draft) === "official";
1017
+ }
979
1018
  function clone(value) {
980
1019
  return JSON.parse(JSON.stringify(value));
981
1020
  }
@@ -1038,7 +1077,16 @@ var require_settings_model = __commonJS({
1038
1077
  const value = typeof rawValue === "string" ? rawValue.trim() : rawValue;
1039
1078
  switch (id) {
1040
1079
  case "mode": {
1041
- if (!ACCESS_MODES2.includes(value)) return { draft, errorKey: "error.invalidMode" };
1080
+ if (!SELECTABLE_MODES2.includes(value)) return { draft, errorKey: "error.invalidMode" };
1081
+ if (value === "official") {
1082
+ next.relay.mode = "remote";
1083
+ next.relay.remoteUrl = OFFICIAL_RELAY_URL2;
1084
+ next.relay.publicUrl = EMPTY;
1085
+ break;
1086
+ }
1087
+ if (value === "remote" && next.relay.remoteUrl === OFFICIAL_RELAY_URL2) {
1088
+ next.relay.remoteUrl = EMPTY;
1089
+ }
1042
1090
  next.relay.mode = value;
1043
1091
  if (value === "lan" && (isLoopbackHost(next.relay.lanHost) || isUnspecifiedAddress(next.relay.lanHost))) {
1044
1092
  next.relay.lanHost = EMPTY;
@@ -1139,8 +1187,11 @@ var require_settings_model = __commonJS({
1139
1187
  }
1140
1188
  module.exports = {
1141
1189
  FIELDS: FIELDS2,
1190
+ SELECTABLE_MODES: SELECTABLE_MODES2,
1142
1191
  createDraft: createDraft2,
1143
1192
  fieldsForMode: fieldsForMode2,
1193
+ isOfficialRelay: isOfficialRelay2,
1194
+ selectedMode: selectedMode2,
1144
1195
  getField: getField2,
1145
1196
  getFieldPlaceholder: getFieldPlaceholder2,
1146
1197
  setField: setField2,
@@ -1521,7 +1572,11 @@ var require_service = __commonJS({
1521
1572
  RELAY_PASSWORD: state.hostToken,
1522
1573
  RELAY_AUTH_STATE_FILE: relayAuthStatePath(),
1523
1574
  RELAY_ALLOWED_ORIGINS: (config.relay.allowedOrigins || []).join(","),
1524
- RELAY_MAX_CLIENTS_PER_HOST: String(config.relay.maxClientsPerHost)
1575
+ RELAY_MAX_CLIENTS_PER_HOST: String(config.relay.maxClientsPerHost),
1576
+ RELAY_MAX_HOSTS: String(config.relay.maxHosts),
1577
+ RELAY_MAX_PENDING_HANDSHAKES: String(config.relay.maxPendingHandshakes),
1578
+ RELAY_MAX_BUFFERED_BYTES_PER_CLIENT: String(config.relay.maxBufferedBytesPerClient),
1579
+ RELAY_HOST_RECONNECT_GRACE_MS: String(config.relay.hostReconnectGraceMs)
1525
1580
  }
1526
1581
  });
1527
1582
  }
@@ -1704,18 +1759,26 @@ var require_service = __commonJS({
1704
1759
  throw lastError || new Error("relay did not become ready");
1705
1760
  }
1706
1761
  async function waitForHost(config, { attempts = 30, delayMs = 100 } = {}) {
1707
- let lastHealth = null;
1762
+ const state = ensureRuntime2();
1763
+ const statusEndpoint = `${resolveAdminOrigin2(config)}/api/status`;
1764
+ let lastStatus = null;
1708
1765
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1709
1766
  try {
1710
- lastHealth = await requestJson2(healthUrl(config), { timeout: 800 });
1711
- if (lastHealth.hosts > 0) return lastHealth;
1767
+ lastStatus = await requestJson2(statusEndpoint, {
1768
+ timeout: 800,
1769
+ headers: {
1770
+ "X-Herdr-Host-Id": state.hostId,
1771
+ "X-Herdr-Host-Token": state.hostToken
1772
+ }
1773
+ });
1774
+ if (lastStatus.hosts?.some((host) => host.id === state.hostId)) return lastStatus;
1712
1775
  } catch (error2) {
1713
- lastHealth = { ok: false, message: error2.message };
1776
+ lastStatus = { ok: false, message: error2.message };
1714
1777
  }
1715
1778
  await new Promise((resolve) => setTimeout(resolve, delayMs));
1716
1779
  }
1717
- const error = new Error(lastHealth?.message || "the Herdr host connector did not register with the relay");
1718
- error.health = lastHealth;
1780
+ const error = new Error(lastStatus?.message || "the Herdr host connector did not register with the relay");
1781
+ error.health = lastStatus;
1719
1782
  throw error;
1720
1783
  }
1721
1784
  async function statusServices() {
@@ -2301,11 +2364,13 @@ var require_updater = __commonJS({
2301
2364
  "src/updater.js"(exports, module) {
2302
2365
  "use strict";
2303
2366
  var fs = __require("node:fs");
2367
+ var os = __require("node:os");
2304
2368
  var path = __require("node:path");
2305
2369
  var { spawn } = __require("node:child_process");
2306
2370
  var { PACKAGE_ROOT } = require_config();
2307
2371
  var PACKAGE_NAME = "herdr-remote";
2308
- var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
2372
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
2373
+ var MIRROR_REGISTRY = "https://registry.npmmirror.com";
2309
2374
  function currentVersion() {
2310
2375
  try {
2311
2376
  return __require(path.join(PACKAGE_ROOT, "package.json")).version;
@@ -2325,6 +2390,43 @@ var require_updater = __commonJS({
2325
2390
  function canSelfUpdate2() {
2326
2391
  return installKind2() === "npm";
2327
2392
  }
2393
+ function normalizeRegistry(value) {
2394
+ if (typeof value !== "string") return null;
2395
+ const trimmed = value.trim().replace(/\/+$/, "");
2396
+ if (!/^https?:\/\//i.test(trimmed)) return null;
2397
+ return trimmed;
2398
+ }
2399
+ function registryFromNpmrc(filePath) {
2400
+ let contents;
2401
+ try {
2402
+ contents = fs.readFileSync(filePath, "utf8");
2403
+ } catch {
2404
+ return null;
2405
+ }
2406
+ let found = null;
2407
+ for (const rawLine of contents.split(/\r?\n/)) {
2408
+ const line = rawLine.trim();
2409
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
2410
+ const match = /^registry\s*=\s*(.+)$/i.exec(line);
2411
+ if (match) found = normalizeRegistry(match[1].replace(/^["']|["']$/g, ""));
2412
+ }
2413
+ return found;
2414
+ }
2415
+ function registryCandidates({ env = process.env, home = os.homedir(), cwd = process.cwd() } = {}) {
2416
+ const candidates = [];
2417
+ const add = (value) => {
2418
+ const normalized = normalizeRegistry(value);
2419
+ if (normalized && !candidates.includes(normalized)) candidates.push(normalized);
2420
+ };
2421
+ add(env.npm_config_registry);
2422
+ add(env.NPM_CONFIG_REGISTRY);
2423
+ add(env.HERDR_REMOTE_REGISTRY);
2424
+ add(registryFromNpmrc(path.join(cwd, ".npmrc")));
2425
+ add(registryFromNpmrc(path.join(home, ".npmrc")));
2426
+ add(DEFAULT_REGISTRY);
2427
+ add(MIRROR_REGISTRY);
2428
+ return candidates;
2429
+ }
2328
2430
  function compareVersions(a, b) {
2329
2431
  const parse = (value) => String(value).split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
2330
2432
  const left = parse(a);
@@ -2336,39 +2438,64 @@ var require_updater = __commonJS({
2336
2438
  }
2337
2439
  return 0;
2338
2440
  }
2339
- async function checkForUpdate2({ timeoutMs = 8e3, fetchImpl = globalThis.fetch } = {}) {
2340
- const current = currentVersion();
2441
+ async function askRegistry(registry, { timeoutMs, fetchImpl }) {
2341
2442
  const controller = new AbortController();
2342
2443
  const timer = setTimeout(() => controller.abort(), timeoutMs);
2343
2444
  try {
2344
- const response = await fetchImpl(REGISTRY_URL, {
2445
+ const response = await fetchImpl(`${registry}/${PACKAGE_NAME}/latest`, {
2345
2446
  signal: controller.signal,
2346
2447
  headers: { Accept: "application/vnd.npm.install-v1+json" }
2347
2448
  });
2348
- if (!response.ok) {
2349
- return { ok: false, current, errorKey: "update.errorNetwork" };
2350
- }
2449
+ if (!response.ok) return { ok: false, message: `HTTP ${response.status ?? "?"}` };
2351
2450
  const body = await response.json();
2352
2451
  const latest = typeof body?.version === "string" ? body.version : null;
2353
- if (!latest) return { ok: false, current, errorKey: "update.errorNetwork" };
2354
- return {
2355
- ok: true,
2356
- current,
2357
- latest,
2358
- updateAvailable: compareVersions(latest, current) > 0
2359
- };
2360
- } catch {
2361
- return { ok: false, current, errorKey: "update.errorNetwork" };
2452
+ if (!latest) return { ok: false, message: "registry answered without a version" };
2453
+ return { ok: true, latest };
2454
+ } catch (error) {
2455
+ return { ok: false, message: error?.name === "AbortError" ? "timed out" : String(error?.message || error) };
2362
2456
  } finally {
2363
2457
  clearTimeout(timer);
2364
2458
  }
2365
2459
  }
2460
+ async function checkForUpdate2({
2461
+ timeoutMs = 6e3,
2462
+ fetchImpl = globalThis.fetch,
2463
+ registries = registryCandidates()
2464
+ } = {}) {
2465
+ const current = currentVersion();
2466
+ const failures = [];
2467
+ for (const registry of registries) {
2468
+ const attempt = await askRegistry(registry, { timeoutMs, fetchImpl });
2469
+ if (attempt.ok) {
2470
+ return {
2471
+ ok: true,
2472
+ current,
2473
+ latest: attempt.latest,
2474
+ registry,
2475
+ updateAvailable: compareVersions(attempt.latest, current) > 0
2476
+ };
2477
+ }
2478
+ failures.push(`${registry}: ${attempt.message}`);
2479
+ }
2480
+ return {
2481
+ ok: false,
2482
+ current,
2483
+ errorKey: "update.errorNetwork",
2484
+ message: failures.join("; "),
2485
+ triedRegistries: registries
2486
+ };
2487
+ }
2366
2488
  function performUpdate2({
2367
2489
  spawnImpl = spawn,
2368
2490
  timeoutMs = 18e4,
2369
2491
  // Seam for tests: the suite runs from a checkout, where the guard below is
2370
2492
  // correctly the only reachable outcome.
2371
- installKindImpl = installKind2
2493
+ installKindImpl = installKind2,
2494
+ // The registry that just answered the version check. Installing from
2495
+ // somewhere the machine has been shown to reach beats installing from a
2496
+ // default it may have no route to. Empty means "whatever npm is configured
2497
+ // with", which is the right answer when no check has run.
2498
+ registry = ""
2372
2499
  } = {}) {
2373
2500
  return new Promise((resolve) => {
2374
2501
  const kind = installKindImpl();
@@ -2376,7 +2503,10 @@ var require_updater = __commonJS({
2376
2503
  resolve({ ok: false, errorKey: `update.cannot.${kind}` });
2377
2504
  return;
2378
2505
  }
2379
- const child = spawnImpl("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
2506
+ const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
2507
+ const normalizedRegistry = normalizeRegistry(registry);
2508
+ if (normalizedRegistry) args.push("--registry", normalizedRegistry);
2509
+ const child = spawnImpl("npm", args, {
2380
2510
  encoding: "utf8",
2381
2511
  timeout: timeoutMs
2382
2512
  });
@@ -2396,12 +2526,15 @@ var require_updater = __commonJS({
2396
2526
  }
2397
2527
  module.exports = {
2398
2528
  PACKAGE_NAME,
2529
+ DEFAULT_REGISTRY,
2530
+ MIRROR_REGISTRY,
2399
2531
  canSelfUpdate: canSelfUpdate2,
2400
2532
  checkForUpdate: checkForUpdate2,
2401
2533
  compareVersions,
2402
2534
  currentVersion,
2403
2535
  installKind: installKind2,
2404
- performUpdate: performUpdate2
2536
+ performUpdate: performUpdate2,
2537
+ registryCandidates
2405
2538
  };
2406
2539
  }
2407
2540
  });
@@ -2728,12 +2861,26 @@ function relayStartCommand(config, password) {
2728
2861
  return parts.join(" ");
2729
2862
  }
2730
2863
  async function probeRelay(config) {
2864
+ const origin = (0, import_config.resolveAdminOrigin)(config);
2865
+ let health;
2731
2866
  try {
2732
- const health = await (0, import_service.requestJson)(`${(0, import_config.resolveAdminOrigin)(config)}/healthz`, { timeout: 4e3 });
2733
- return { ok: true, version: health.version, hosts: health.hosts };
2867
+ health = await (0, import_service.requestJson)(`${origin}/healthz`, { timeout: 4e3 });
2734
2868
  } catch (error) {
2735
2869
  return { ok: false, message: error.message };
2736
2870
  }
2871
+ try {
2872
+ const runtime = (0, import_service.ensureRuntime)();
2873
+ const status = await (0, import_service.requestJson)(`${origin}/api/status`, {
2874
+ timeout: 4e3,
2875
+ headers: {
2876
+ "X-Herdr-Host-Id": runtime.hostId,
2877
+ "X-Herdr-Host-Token": runtime.hostToken
2878
+ }
2879
+ });
2880
+ return { ok: true, version: String(health.version || status.version || ""), hosts: Array.isArray(status.hosts) ? status.hosts.length : 0 };
2881
+ } catch {
2882
+ return { ok: true, version: String(health.version || ""), hosts: void 0 };
2883
+ }
2737
2884
  }
2738
2885
  function formatUptime(seconds, t) {
2739
2886
  if (!Number.isFinite(seconds)) return t("common.unknown");
@@ -3105,6 +3252,7 @@ function RelayScreen({ ctx }) {
3105
3252
  const [revealTokens, setRevealTokens] = useState5(false);
3106
3253
  const [showEnv, setShowEnv] = useState5(false);
3107
3254
  const [password, setPassword] = useState5(ctx.runtime.relayPassword || "");
3255
+ const official = (0, import_settings_model.isOfficialRelay)(draft);
3108
3256
  const fields = useMemo2(
3109
3257
  () => (0, import_settings_model.fieldsForMode)(draft.relay.mode).filter((field) => ["mode", "port", "lanHost", "remoteUrl", "publicUrl"].includes(field.id)),
3110
3258
  [draft.relay.mode]
@@ -3112,20 +3260,24 @@ function RelayScreen({ ctx }) {
3112
3260
  const entries = [
3113
3261
  ...fields.map((field) => ({
3114
3262
  id: field.id,
3115
- kind: "field",
3263
+ // The official relay's address is ours and its browser URL follows from
3264
+ // it, so neither is offered for editing: an editable box holding a value
3265
+ // that must not change is an invitation to break the setup.
3266
+ kind: official && (field.id === "remoteUrl" || field.id === "publicUrl") ? "readonly" : "field",
3116
3267
  fieldKind: field.kind,
3117
3268
  label: t(field.labelKey)
3118
3269
  })),
3119
3270
  // The password only matters when talking to a relay somebody else started;
3120
- // a local relay is configured with our own token automatically.
3121
- ...draft.relay.mode === "remote" ? [{ id: "password", kind: "password", label: t("relay.password") }] : [],
3271
+ // a local relay is configured with our own token automatically, and the
3272
+ // official one authenticates workstations by their own host token.
3273
+ ...draft.relay.mode === "remote" && !official ? [{ id: "password", kind: "password", label: t("relay.password") }] : [],
3122
3274
  // Saving needs a row of its own. Every field here only edits a draft, and
3123
3275
  // the services restart from what is on disk — with `s` as the sole way to
3124
3276
  // commit, a change could look applied, survive a restart, and never take
3125
3277
  // effect. It is also the only way to save with the mouse.
3126
3278
  { id: "save", kind: "action", label: t("common.save") },
3127
3279
  { id: "test", kind: "action", label: t("relay.test") },
3128
- ...draft.relay.mode === "remote" ? [
3280
+ ...draft.relay.mode === "remote" && !official ? [
3129
3281
  { id: "reveal", kind: "action", label: revealTokens ? t("relay.hidePassword") : t("relay.showPassword") },
3130
3282
  { id: "env", kind: "action", label: t("relay.envSnippet") }
3131
3283
  ] : [],
@@ -3160,6 +3312,10 @@ function RelayScreen({ ctx }) {
3160
3312
  const activate = (id) => {
3161
3313
  const entry = entries.find((candidate) => candidate.id === id);
3162
3314
  if (!entry) return;
3315
+ if (entry.kind === "readonly") {
3316
+ ctx.notify(t("relay.officialFixed"), "info");
3317
+ return;
3318
+ }
3163
3319
  if (entry.kind === "field" || entry.kind === "password") {
3164
3320
  ctx.setEditing(id);
3165
3321
  return;
@@ -3214,14 +3370,13 @@ function RelayScreen({ ctx }) {
3214
3370
  { id: "official", label: t("mode.official"), description: t("mode.official.description") },
3215
3371
  { id: "remote", label: t("mode.remote"), description: t("mode.remote.description") }
3216
3372
  ],
3217
- current: draft.relay.mode === "remote" && draft.relay.remoteUrl === import_config.OFFICIAL_RELAY_URL ? "official" : draft.relay.mode,
3373
+ current: (0, import_settings_model.selectedMode)(draft),
3218
3374
  onPick: (choice) => {
3375
+ if (!applyField("mode", choice)) return;
3219
3376
  if (choice === "official") {
3220
- applyFields([["mode", "remote"], ["remoteUrl", import_config.OFFICIAL_RELAY_URL]]);
3221
- ctx.setEditing(null);
3222
- return;
3377
+ setPassword("");
3378
+ (0, import_service.setRelayPassword)("");
3223
3379
  }
3224
- applyField("mode", choice);
3225
3380
  ctx.setEditing(null);
3226
3381
  },
3227
3382
  onCancel: () => ctx.setEditing(null)
@@ -3285,10 +3440,14 @@ function RelayScreen({ ctx }) {
3285
3440
  },
3286
3441
  onCancel: () => ctx.setEditing(null)
3287
3442
  }
3443
+ ) : entry.kind === "readonly" ? (
3444
+ // Fixed by us, so it is printed rather than offered for editing.
3445
+ /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: `${(0, import_settings_model.getField)(draft, entry.id) || placeholderText(ctx, entry.id)} (${t("relay.locked")})` })
3288
3446
  ) : entry.fieldKind === "choice" ? (
3289
3447
  // Choice fields are picked from a list, never typed, so they
3290
- // show the translated label rather than the stored id.
3291
- /* @__PURE__ */ jsx7(Text6, { children: t(`mode.${(0, import_settings_model.getField)(draft, entry.id)}`) })
3448
+ // show the translated label rather than the stored id — and the
3449
+ // official relay is named as itself, not as "self-hosted".
3450
+ /* @__PURE__ */ jsx7(Text6, { children: t(`mode.${(0, import_settings_model.selectedMode)(draft)}`) })
3292
3451
  ) : /* @__PURE__ */ jsx7(
3293
3452
  TextField,
3294
3453
  {
@@ -3305,13 +3464,13 @@ function RelayScreen({ ctx }) {
3305
3464
  entry.id
3306
3465
  )),
3307
3466
  ctx.dirty ? /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.warn, children: t("hint.unsavedChanges") }) }) : null,
3308
- draft.relay.mode === "remote" ? /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.passwordHint") }) }) : null,
3467
+ draft.relay.mode === "remote" ? /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: official ? t("relay.officialHint") : t("relay.passwordHint") }) }) : null,
3309
3468
  /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Row, { label: t("relay.identity"), children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: ctx.runtime.hostId }) }) }),
3310
3469
  showEnv ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [
3311
3470
  /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.envHint") }),
3312
3471
  /* @__PURE__ */ jsx7(Text6, { children: relayStartCommand(ctx.config, password) })
3313
3472
  ] }) : null,
3314
- /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.docsHint") }) }),
3473
+ official ? null : /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text6, { color: theme.muted, children: t("relay.docsHint") }) }),
3315
3474
  /* @__PURE__ */ jsx7(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
3316
3475
  ] });
3317
3476
  }
@@ -3603,6 +3762,7 @@ function About({ ctx }) {
3603
3762
  const { t, draft } = ctx;
3604
3763
  const [selected, setSelected] = useState8("auto");
3605
3764
  const [update, setUpdate] = useState8({ phase: "idle" });
3765
+ const registryRef = useState8(() => ({ current: "" }))[0];
3606
3766
  const detected = detectLocale({ preference: "auto" });
3607
3767
  const languageOptions = [
3608
3768
  { id: "auto", label: t("about.languageAuto", { detected }) },
@@ -3634,13 +3794,15 @@ function About({ ctx }) {
3634
3794
  const result = await (0, import_updater.checkForUpdate)();
3635
3795
  if (!result.ok) {
3636
3796
  setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorNetwork" });
3797
+ if (result.message) ctx.notify(t("update.errorNetworkDetail", { message: result.message }), "error");
3637
3798
  return;
3638
3799
  }
3800
+ registryRef.current = result.registry ?? "";
3639
3801
  setUpdate(result.updateAvailable ? { phase: "available", latest: result.latest } : { phase: "current", latest: result.latest });
3640
3802
  };
3641
3803
  const runUpdate = async (latest) => {
3642
3804
  setUpdate({ phase: "updating", latest });
3643
- const result = await (0, import_updater.performUpdate)();
3805
+ const result = await (0, import_updater.performUpdate)({ registry: registryRef.current });
3644
3806
  if (!result.ok) {
3645
3807
  setUpdate({ phase: "error", messageKey: result.errorKey ?? "update.errorFailed" });
3646
3808
  ctx.notify(t("update.errorFailed"), "error");
@@ -3717,7 +3879,7 @@ function About({ ctx }) {
3717
3879
  children: updateLabel
3718
3880
  }
3719
3881
  ) }),
3720
- /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.3" }) }),
3882
+ /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.5" }) }),
3721
3883
  /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
3722
3884
  /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
3723
3885
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
@@ -3848,16 +4010,16 @@ function Wizard({ ctx, onDone }) {
3848
4010
  description: t("mode.remote.description")
3849
4011
  }
3850
4012
  ],
3851
- current: draft.relay.mode === "remote" && draft.relay.remoteUrl === import_config.OFFICIAL_RELAY_URL ? "official" : draft.relay.mode,
4013
+ current: (0, import_settings_model.selectedMode)(draft),
3852
4014
  onPick: (choice) => {
3853
4015
  if (choice === "official") {
3854
- if (!applyAll([["mode", "remote"], ["remoteUrl", import_config.OFFICIAL_RELAY_URL]])) return;
4016
+ if (!apply("mode", "official")) return;
3855
4017
  (0, import_service.setRelayPassword)("");
3856
4018
  advance("access", stepsFor("remote", import_config.OFFICIAL_RELAY_URL));
3857
4019
  return;
3858
4020
  }
3859
4021
  if (apply("mode", choice)) {
3860
- advance("access", stepsFor(choice, draft.relay.remoteUrl));
4022
+ advance("access", stepsFor(choice, choice === "remote" ? "" : draft.relay.remoteUrl));
3861
4023
  }
3862
4024
  },
3863
4025
  onCancel: back