herdr-remote-relay 0.3.23 → 0.3.25

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.
@@ -17,6 +17,19 @@ function nowIso(now = Date.now()) {
17
17
  function validHostId(value) {
18
18
  return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
19
19
  }
20
+ /**
21
+ * What a person reads off one screen and types on another: six characters,
22
+ * without 0/O or 1/I/L to confuse. Cutting base64 and dropping its `-` and `_`
23
+ * made one code in seven shorter than six, and some too short to pair at all.
24
+ */
25
+ const PAIR_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
26
+ const PAIR_CODE_LENGTH = 6;
27
+ function newPairCode() {
28
+ let code = '';
29
+ for (let i = 0; i < PAIR_CODE_LENGTH; i += 1)
30
+ code += PAIR_CODE_ALPHABET[crypto.randomInt(PAIR_CODE_ALPHABET.length)];
31
+ return code;
32
+ }
20
33
  class AuthStore {
21
34
  stateFile;
22
35
  pairingTtlMs;
@@ -119,7 +132,7 @@ class AuthStore {
119
132
  this.cleanup(now);
120
133
  let code;
121
134
  do {
122
- code = randomToken(4).toUpperCase().replace(/[-_]/g, '').slice(0, 6);
135
+ code = newPairCode();
123
136
  } while ([...this.pairings.values()].some((pairing) => equalHash(pairing.codeHash, hash(code))));
124
137
  const expiresAt = now + this.pairingTtlMs;
125
138
  this.pairings.set(code, {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Validates the Herdr prefix keys crossing the wire, or read from a config
3
+ * file, into one canonical list: lowercase, no whitespace, no duplicates, at
4
+ * most `MAX_HERDR_PREFIX_KEYS` short entries. Herdr ignores entries it cannot
5
+ * read and so does this. `undefined` when nothing usable is left, so a host
6
+ * that says nothing and one that says garbage look the same.
7
+ */
8
+ export declare function sanitizeHerdrPrefixKeys(value: unknown): string[] | undefined;
@@ -0,0 +1,34 @@
1
+ // The keys that put the workstation's Herdr into prefix mode, as a host reads
2
+ // them from Herdr's config and a browser receives them. Browser-safe: no Node
3
+ // APIs, so the web app imports it.
4
+ /** Herdr accepts any number of prefix keys; a window has no use for more than this. */
5
+ const MAX_HERDR_PREFIX_KEYS = 8;
6
+ /** Longest real combo is `ctrl+shift+double_quote`; this leaves room for spelling variants. */
7
+ const MAX_HERDR_PREFIX_KEY_LENGTH = 32;
8
+ /**
9
+ * What the browser can act on: a key combo is printable ASCII (modifiers and
10
+ * key names joined by `+`). Whitespace is removed before this is tested.
11
+ */
12
+ const PREFIX_KEY = /^[\x21-\x7e]+$/;
13
+ /**
14
+ * Validates the Herdr prefix keys crossing the wire, or read from a config
15
+ * file, into one canonical list: lowercase, no whitespace, no duplicates, at
16
+ * most `MAX_HERDR_PREFIX_KEYS` short entries. Herdr ignores entries it cannot
17
+ * read and so does this. `undefined` when nothing usable is left, so a host
18
+ * that says nothing and one that says garbage look the same.
19
+ */
20
+ export function sanitizeHerdrPrefixKeys(value) {
21
+ if (!Array.isArray(value))
22
+ return undefined;
23
+ const keys = new Set();
24
+ for (const entry of value) {
25
+ if (keys.size >= MAX_HERDR_PREFIX_KEYS)
26
+ break;
27
+ if (typeof entry !== 'string')
28
+ continue;
29
+ const key = entry.replace(/\s+/g, '').toLowerCase();
30
+ if (key.length <= MAX_HERDR_PREFIX_KEY_LENGTH && PREFIX_KEY.test(key))
31
+ keys.add(key);
32
+ }
33
+ return keys.size > 0 ? [...keys] : undefined;
34
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './frames.js';
2
2
  export * from './terminal.js';
3
+ export * from './herdr-keys.js';
3
4
  export * from './messages.js';
4
5
  export * from './paste.js';
5
6
  export type * from './http.js';
@@ -3,5 +3,6 @@
3
3
  // packages build against; nothing here may be copied into the CLI or the web.
4
4
  export * from './frames.js';
5
5
  export * from './terminal.js';
6
+ export * from './herdr-keys.js';
6
7
  export * from './messages.js';
7
8
  export * from './paste.js';
@@ -94,6 +94,11 @@ export interface ServerReadyMessage {
94
94
  clientCount?: number;
95
95
  /** The workstation's herdr-remote release; null when it is too old to say. */
96
96
  hostVersion?: string | null;
97
+ /**
98
+ * The keys that put the workstation's Herdr into prefix mode, lowercase,
99
+ * like `ctrl+b`. Left out when the host is too old to say.
100
+ */
101
+ herdrPrefixKeys?: string[];
97
102
  }
98
103
  export interface ServerPairedMessage {
99
104
  type: 'paired';
@@ -133,6 +138,8 @@ export interface ServerSessionRestartedMessage {
133
138
  terminalFont?: HostTerminalFont | null;
134
139
  /** As in `ready`: the host may have come back on another release. */
135
140
  hostVersion?: string | null;
141
+ /** As in `ready`: the host may have come back with another Herdr config. */
142
+ herdrPrefixKeys?: string[];
136
143
  }
137
144
  export interface ServerExitMessage {
138
145
  type: 'exit';
@@ -239,6 +246,11 @@ export interface HostHelloMessage {
239
246
  version?: string;
240
247
  terminalPalette?: HostTerminalPalette | null;
241
248
  terminalFont?: HostTerminalFont | null;
249
+ /**
250
+ * The keys `keys.prefix` in the workstation's Herdr config names, as the
251
+ * host read them. The relay sanitizes them; older connectors leave them out.
252
+ */
253
+ herdrPrefixKeys?: string[];
242
254
  capabilities?: string[];
243
255
  }
244
256
  /** One PTY the host is running, as the relay's status page lists it. */
@@ -148,6 +148,7 @@ export function handleClientConnection(relay, ws, req) {
148
148
  terminalFont: host.terminalFont || null,
149
149
  clientCount: host.clients.size,
150
150
  hostVersion: host.version,
151
+ ...(host.herdrPrefixKeys ? { herdrPrefixKeys: host.herdrPrefixKeys } : {}),
151
152
  });
152
153
  if (host.agentStatus)
153
154
  jsonSend(ws, host.agentStatus);
@@ -1,7 +1,7 @@
1
1
  // The host side of the relay: a workstation's WebSocket, from its hello and
2
2
  // the hand-off from an older connection to every message it sends after.
3
3
  import os from 'node:os';
4
- import { CAPABILITY, PROTOCOL_VERSION, sanitizeTerminalFont, sanitizeTerminalPalette, unpackStreamFrame, } from '../protocol/index.js';
4
+ import { CAPABILITY, PROTOCOL_VERSION, sanitizeHerdrPrefixKeys, sanitizeTerminalFont, sanitizeTerminalPalette, unpackStreamFrame, } from '../protocol/index.js';
5
5
  import { forwardFontChunk, forwardFontSubset, settleFontChunk, settleFontSubset, } from './font-proxy.js';
6
6
  import { broadcastAgentStatus, broadcastTerminalFont, broadcastUpdateStatus, releaseVersion, } from './host-broadcasts.js';
7
7
  import { allowHostHandshake } from './requests.js';
@@ -10,6 +10,7 @@ import { closeSocket, isOpen, jsonSend, parseJson, randomId, rejectHandshake } f
10
10
  import { broadcastToClients, notifyHostClientCount, sendClientBinary } from './transport.js';
11
11
  function createHostRecord(message, ws, pending, clients = new Set()) {
12
12
  const capabilities = Array.isArray(message.capabilities) ? message.capabilities : [];
13
+ const herdrPrefixKeys = sanitizeHerdrPrefixKeys(message.herdrPrefixKeys);
13
14
  return {
14
15
  id: message.hostId,
15
16
  ws,
@@ -23,6 +24,7 @@ function createHostRecord(message, ws, pending, clients = new Set()) {
23
24
  terminalPalette: sanitizeTerminalPalette(message.terminalPalette),
24
25
  /** Family, size and fetchable files of the workstation's terminal font. */
25
26
  terminalFont: sanitizeTerminalFont(message.terminalFont),
27
+ ...(herdrPrefixKeys ? { herdrPrefixKeys } : {}),
26
28
  /** The latest workstation snapshot is replayed when a browser joins late. */
27
29
  agentStatus: null,
28
30
  /** Whether the workstation's herdr-remote is behind; replayed the same way. */
@@ -72,6 +72,7 @@ export function startSession(relay, host, client, { restarted = false } = {}) {
72
72
  terminalPalette: host.terminalPalette || null,
73
73
  terminalFont: host.terminalFont || null,
74
74
  hostVersion: host.version,
75
+ ...(host.herdrPrefixKeys ? { herdrPrefixKeys: host.herdrPrefixKeys } : {}),
75
76
  });
76
77
  }
77
78
  }
@@ -31,6 +31,8 @@ export interface RelayHost {
31
31
  connectedAtMs: number;
32
32
  terminalPalette: HostTerminalPalette | null;
33
33
  terminalFont: HostTerminalFont | null;
34
+ /** Sanitized `keys.prefix` of the host's Herdr; absent when the host did not say. */
35
+ herdrPrefixKeys?: string[];
34
36
  /** The latest agent summary, replayed to a window that joins late. */
35
37
  agentStatus: ServerAgentStatusMessage | null;
36
38
  /** Whether the workstation's herdr-remote is behind; replayed the same way. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herdr-remote-relay",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Standalone relay server and web terminal for Herdr Remote",
5
5
  "license": "MIT",
6
6
  "type": "module",