ai-remote 0.1.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.
Files changed (114) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/protocols/index.js +35 -0
  5. package/dist/protocols/index.js.map +7 -0
  6. package/dist/protocols/rdp/buffer.js +170 -0
  7. package/dist/protocols/rdp/buffer.js.map +7 -0
  8. package/dist/protocols/rdp/caps.js +260 -0
  9. package/dist/protocols/rdp/caps.js.map +7 -0
  10. package/dist/protocols/rdp/cert.js +135 -0
  11. package/dist/protocols/rdp/cert.js.map +7 -0
  12. package/dist/protocols/rdp/client.js +1068 -0
  13. package/dist/protocols/rdp/client.js.map +7 -0
  14. package/dist/protocols/rdp/cliprdr.js +240 -0
  15. package/dist/protocols/rdp/cliprdr.js.map +7 -0
  16. package/dist/protocols/rdp/credssp.js +254 -0
  17. package/dist/protocols/rdp/credssp.js.map +7 -0
  18. package/dist/protocols/rdp/crypto.js +369 -0
  19. package/dist/protocols/rdp/crypto.js.map +7 -0
  20. package/dist/protocols/rdp/display.js +589 -0
  21. package/dist/protocols/rdp/display.js.map +7 -0
  22. package/dist/protocols/rdp/gcc.js +132 -0
  23. package/dist/protocols/rdp/gcc.js.map +7 -0
  24. package/dist/protocols/rdp/keymap.js +132 -0
  25. package/dist/protocols/rdp/keymap.js.map +7 -0
  26. package/dist/protocols/rdp/mcs.js +223 -0
  27. package/dist/protocols/rdp/mcs.js.map +7 -0
  28. package/dist/protocols/rdp/ntlm.js +305 -0
  29. package/dist/protocols/rdp/ntlm.js.map +7 -0
  30. package/dist/protocols/rdp/pdu.js +444 -0
  31. package/dist/protocols/rdp/pdu.js.map +7 -0
  32. package/dist/protocols/rdp/rail.js +383 -0
  33. package/dist/protocols/rdp/rail.js.map +7 -0
  34. package/dist/protocols/rdp/rle.js +276 -0
  35. package/dist/protocols/rdp/rle.js.map +7 -0
  36. package/dist/protocols/rdp/sec.js +156 -0
  37. package/dist/protocols/rdp/sec.js.map +7 -0
  38. package/dist/protocols/rdp/session.js +307 -0
  39. package/dist/protocols/rdp/session.js.map +7 -0
  40. package/dist/protocols/rdp/tls.js +354 -0
  41. package/dist/protocols/rdp/tls.js.map +7 -0
  42. package/dist/protocols/rdp/vchannel.js +59 -0
  43. package/dist/protocols/rdp/vchannel.js.map +7 -0
  44. package/dist/protocols/rdp/x224.js +110 -0
  45. package/dist/protocols/rdp/x224.js.map +7 -0
  46. package/dist/protocols/ssh/kex.js +177 -0
  47. package/dist/protocols/ssh/kex.js.map +7 -0
  48. package/dist/protocols/ssh/messages.js +55 -0
  49. package/dist/protocols/ssh/messages.js.map +7 -0
  50. package/dist/protocols/ssh/session.js +608 -0
  51. package/dist/protocols/ssh/session.js.map +7 -0
  52. package/dist/protocols/ssh/terminal.js +191 -0
  53. package/dist/protocols/ssh/terminal.js.map +7 -0
  54. package/dist/protocols/ssh/transport.js +611 -0
  55. package/dist/protocols/ssh/transport.js.map +7 -0
  56. package/dist/protocols/ssh/wire.js +189 -0
  57. package/dist/protocols/ssh/wire.js.map +7 -0
  58. package/dist/protocols/types.js +1 -0
  59. package/dist/protocols/types.js.map +7 -0
  60. package/dist/protocols/vnc/input.js +16 -0
  61. package/dist/protocols/vnc/input.js.map +7 -0
  62. package/dist/protocols/vnc/session.js +259 -0
  63. package/dist/protocols/vnc/session.js.map +7 -0
  64. package/dist/shared/connection.js +114 -0
  65. package/dist/shared/connection.js.map +7 -0
  66. package/dist/shared/device.js +1 -0
  67. package/dist/shared/device.js.map +7 -0
  68. package/dist/shared/hosts.js +133 -0
  69. package/dist/shared/hosts.js.map +7 -0
  70. package/dist/shared/icons.js +42 -0
  71. package/dist/shared/icons.js.map +7 -0
  72. package/dist/shared/protocol.js +40 -0
  73. package/dist/shared/protocol.js.map +7 -0
  74. package/dist/shared/signals.js +42 -0
  75. package/dist/shared/signals.js.map +7 -0
  76. package/package.json +82 -0
  77. package/src/index.ts +14 -0
  78. package/src/protocols/index.ts +72 -0
  79. package/src/protocols/rdp/buffer.ts +204 -0
  80. package/src/protocols/rdp/caps.ts +304 -0
  81. package/src/protocols/rdp/cert.ts +190 -0
  82. package/src/protocols/rdp/client.ts +1363 -0
  83. package/src/protocols/rdp/cliprdr.ts +344 -0
  84. package/src/protocols/rdp/credssp.ts +319 -0
  85. package/src/protocols/rdp/crypto.ts +416 -0
  86. package/src/protocols/rdp/display.ts +750 -0
  87. package/src/protocols/rdp/gcc.ts +162 -0
  88. package/src/protocols/rdp/keymap.ts +68 -0
  89. package/src/protocols/rdp/mcs.ts +283 -0
  90. package/src/protocols/rdp/ntlm.ts +380 -0
  91. package/src/protocols/rdp/pdu.ts +497 -0
  92. package/src/protocols/rdp/rail.ts +480 -0
  93. package/src/protocols/rdp/rle.ts +343 -0
  94. package/src/protocols/rdp/sec.ts +184 -0
  95. package/src/protocols/rdp/session.ts +393 -0
  96. package/src/protocols/rdp/tls.ts +452 -0
  97. package/src/protocols/rdp/vchannel.ts +89 -0
  98. package/src/protocols/rdp/x224.ts +139 -0
  99. package/src/protocols/ssh/kex.ts +227 -0
  100. package/src/protocols/ssh/messages.ts +54 -0
  101. package/src/protocols/ssh/session.ts +878 -0
  102. package/src/protocols/ssh/terminal.ts +242 -0
  103. package/src/protocols/ssh/transport.ts +732 -0
  104. package/src/protocols/ssh/wire.ts +220 -0
  105. package/src/protocols/types.ts +146 -0
  106. package/src/protocols/vnc/input.ts +13 -0
  107. package/src/protocols/vnc/novnc.d.ts +75 -0
  108. package/src/protocols/vnc/session.ts +367 -0
  109. package/src/shared/connection.ts +206 -0
  110. package/src/shared/device.ts +28 -0
  111. package/src/shared/hosts.ts +240 -0
  112. package/src/shared/icons.ts +49 -0
  113. package/src/shared/protocol.ts +55 -0
  114. package/src/shared/signals.ts +67 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/protocols/vnc/session.ts"],
4
+ "sourcesContent": ["/**\n * Everything VNC-specific, behind the driver interface the RDP side also\n * implements. noVNC's RFB object does the protocol work; this module decides\n * how it is addressed, constructed, controlled and explained.\n *\n * VNC splits the handshake differently from RDP: noVNC hands authentication to\n * the Worker, because macOS Screen Sharing needs the Apple Diffie-Hellman\n * (type 30) exchange that noVNC does not implement. That is why credentials\n * travel in the URL here and never do for RDP.\n */\n\nimport RFB from '@novnc/novnc';\nimport type {\n DialogCopy,\n DisconnectContext,\n PointerButton,\n ProtocolDriver,\n RemoteSession,\n ScreenGeometry,\n SessionOptions,\n} from '../types';\nimport type { ConnectionSettings } from '../../shared/connection';\nimport { DEFAULT_PORTS } from '../../shared/protocol';\nimport {\n GATEWAY_KEEPALIVE_SIGNAL,\n GATEWAY_READY_SIGNAL,\n KEEPALIVE_INTERVAL_MS,\n} from '../../shared/signals';\nimport { vncWheelMasks } from './input';\n\nconst DIALOG: DialogCopy = {\n usernameLabel: 'Username',\n usernamePlaceholder: 'macOS username (optional)',\n usernameHelp: 'Use your macOS account name. Leave it empty when the VNC server only asks for a password.',\n passwordLabel: 'Password',\n passwordPlaceholder: 'Screen sharing password',\n passwordHelp: 'Use the macOS account password, or the password configured for VNC screen sharing.',\n // A VNC session needs some secret; RDP can sign in on the host's own screen.\n requiresPassword: true,\n};\n\nfunction buildUrl(origin: string, settings: ConnectionSettings): string {\n const query = new URLSearchParams({ host: settings.host, port: String(settings.port) });\n if (settings.username) query.set('vnc_user', settings.username);\n if (settings.password) query.set('vnc_pass', settings.password);\n return `${origin}/ws?${query}`;\n}\n\n/** Left = 1, middle = 2, right = 4, as RFB's button mask numbers them. */\nconst BUTTON_MASK: Record<PointerButton, number> = { 0: 1, 1: 2, 2: 4 };\n\nfunction eventDetail(event: Event): Record<string, unknown> {\n if (!('detail' in event)) return {};\n const detail = event.detail;\n if (!detail || typeof detail !== 'object') return {};\n const copy: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(detail)) copy[key] = value;\n return copy;\n}\n\n/**\n * An RFB object given the control surface the RDP session already has, so the\n * AI pane's screen tool never has to ask which protocol it is driving.\n *\n * This is a wrapper rather than a set of properties bolted onto the RFB\n * instance: assigning methods onto someone else's object works until they add a\n * member of the same name, and the type system cannot help when it happens.\n */\nclass VncSession extends EventTarget implements RemoteSession {\n readonly protocol = 'vnc' as const;\n readonly #rfb: RFB;\n readonly #credentials: SessionOptions['credentials'];\n #transport: WebSocket | null = null;\n #keepalive: ReturnType<typeof setInterval> | null = null;\n #readySent = false;\n #close: { code: number; reason: string } | null = null;\n\n constructor(container: HTMLElement, url: string, options: SessionOptions) {\n super();\n\n this.#credentials = options.credentials;\n this.#rfb = new RFB(container, url, {\n credentials: {\n username: options.credentials.username,\n password: options.credentials.password,\n },\n shared: true,\n wsProtocols: ['binary'],\n });\n\n // On touch devices noVNC renders the remote pointer as a fixed canvas on\n // document.body with the highest z-index on the page. Tag that fallback\n // layer so the side panel can hide it; otherwise the Mac's large white\n // pointer stays painted over terminal fields once the panel opens.\n this.#rfb._cursor?._canvas?.classList.add('remote-cursor-overlay');\n\n // Some VNC servers temporarily advertise a fully transparent cursor. Keep\n // the normal remote cursor, but let noVNC's small dot fallback show while\n // the advertised one has no visible pixels, so the pointer never vanishes.\n this.#rfb.showDotCursor = true;\n\n // Resizing the host to match the browser is right on a laptop and wrong on\n // a phone, where it would turn a Mac desktop into a portrait strip.\n this.#rfb.resizeSession = options.allowRemoteResize;\n this.#rfb.clipViewport = true;\n this.#rfb.scaleViewport = options.scaleViewport;\n this.#rfb.viewOnly = options.viewOnly;\n this.#rfb.qualityLevel = options.quality;\n this.#rfb.compressionLevel = options.compression;\n\n // RDP and SSH send these on the socket they own. noVNC owns the VNC\n // socket, so this wrapper has to reach in and do the same: the gateway\n // will not read the host until it sees GATEWAY_READY, and Cloudflare\n // idles out a silent desktop after 100s without keepalives.\n this.#bindTransport();\n\n // Re-dispatch rather than have callers listen to two objects.\n for (const name of ['connect', 'disconnect', 'credentialsrequired', 'desktopname', 'clipboard', 'bell'] as const) {\n this.#rfb.addEventListener(name, (event) => {\n if (name === 'credentialsrequired') {\n this.#rfb.sendCredentials({\n username: this.#credentials.username,\n password: this.#credentials.password,\n });\n }\n\n const detail = eventDetail(event);\n if (name === 'disconnect' && this.#close) {\n detail.code = this.#close.code;\n detail.reason = this.#close.reason;\n }\n this.dispatchEvent(new CustomEvent(name, { detail }));\n });\n }\n }\n\n /**\n * Attach to noVNC's WebSocket once it exists. Construction usually has it\n * already; a retry covers the tick where `_sock._websocket` is still null.\n */\n #bindTransport(): void {\n const attach = (): boolean => {\n const ws = this.#rfb._sock?._websocket ?? null;\n if (!ws || this.#transport === ws) return Boolean(ws);\n this.#transport = ws;\n\n const signalReady = () => {\n if (this.#readySent || ws.readyState !== WebSocket.OPEN) return;\n this.#readySent = true;\n ws.send(GATEWAY_READY_SIGNAL);\n };\n\n ws.addEventListener('open', signalReady);\n // Capture so the close code is stored before noVNC's `onclose` turns\n // into a `disconnect` event that this wrapper re-dispatches.\n ws.addEventListener('close', (event) => {\n this.#close = { code: event.code, reason: event.reason ?? '' };\n this.#stopKeepalive();\n }, true);\n\n this.#keepalive = setInterval(() => {\n if (ws.readyState !== WebSocket.OPEN) return;\n try {\n ws.send(GATEWAY_KEEPALIVE_SIGNAL);\n\n // The text frame above stops at the gateway, so it cannot keep the\n // VPC/Tunnel TCP leg alive. A one-pixel incremental request is a\n // normal RFB message, changes no state, and keeps an idle desktop's\n // origin connection active just as RDP and SSH do with their own\n // protocol-level keepalives.\n const sock = this.#rfb._sock;\n if (sock && this.#rfb._rfbConnectionState === 'connected') {\n RFB.messages.fbUpdateRequest(sock, true, 0, 0, 1, 1);\n }\n } catch {\n // A keepalive is not worth breaking a working session over.\n }\n }, KEEPALIVE_INTERVAL_MS);\n\n signalReady();\n return true;\n };\n\n if (attach()) return;\n\n let tries = 0;\n const timer = setInterval(() => {\n if (attach() || ++tries > 20) clearInterval(timer);\n }, 25);\n }\n\n #stopKeepalive(): void {\n if (this.#keepalive) clearInterval(this.#keepalive);\n this.#keepalive = null;\n }\n\n get connected(): boolean {\n return this.#rfb._rfbConnectionState === 'connected';\n }\n\n get socket(): WebSocket | null {\n return this.#rfb._sock?._websocket ?? null;\n }\n\n get canvas(): HTMLCanvasElement | null {\n return this.#rfb._canvas;\n }\n\n get scaleViewport(): boolean {\n return this.#rfb.scaleViewport;\n }\n\n set scaleViewport(enabled: boolean) {\n this.#rfb.scaleViewport = enabled;\n }\n\n get viewOnly(): boolean {\n return this.#rfb.viewOnly;\n }\n\n set viewOnly(enabled: boolean) {\n this.#rfb.viewOnly = enabled;\n }\n\n get qualityLevel(): number {\n return this.#rfb.qualityLevel;\n }\n\n set qualityLevel(level: number) {\n this.#rfb.qualityLevel = level;\n }\n\n get compressionLevel(): number {\n return this.#rfb.compressionLevel;\n }\n\n set compressionLevel(level: number) {\n this.#rfb.compressionLevel = level;\n }\n\n get screenGeometry(): ScreenGeometry {\n const width = this.#rfb._fbWidth || 0;\n const height = this.#rfb._fbHeight || 0;\n return { width, height, viewport: { x: 0, y: 0, width, height } };\n }\n\n /** Internal state worth logging once the first server frame has been parsed. */\n describeState(): Record<string, unknown> {\n const socket = this.#rfb._sock;\n return {\n connectionState: this.#rfb._rfbConnectionState,\n initializationState: this.#rfb._rfbInitState,\n queuedReceiveBytes: Math.max(0, (socket?._rQlen ?? 0) - (socket?._rQi ?? 0)),\n };\n }\n\n disconnect(): void {\n this.#stopKeepalive();\n this.#rfb.disconnect();\n }\n\n sendKey(keysym: number, code: string | null, down?: boolean): void {\n this.#rfb.sendKey(keysym, code, down);\n }\n\n sendClipboard(text: string): void {\n this.#rfb.clipboardPasteFrom(text);\n }\n\n releaseKeyboard(): void {\n // noVNC ungrabs the keyboard when its canvas loses focus.\n this.#rfb.blur();\n }\n\n /**\n * noVNC keeps pointer input private -- its own handlers translate browser\n * events into framebuffer coordinates first -- so this reaches for the same\n * wire helper `_sendMouse` ends at, with coordinates already in pixels.\n */\n #pointer(x: number, y: number, mask: number): boolean {\n const socket = this.#rfb._sock;\n if (!socket || this.#rfb._rfbConnectionState !== 'connected' || this.#rfb.viewOnly) return false;\n\n const width = this.#rfb._fbWidth || 1;\n const height = this.#rfb._fbHeight || 1;\n RFB.messages.pointerEvent(\n socket,\n Math.max(0, Math.min(width - 1, Math.round(x))),\n Math.max(0, Math.min(height - 1, Math.round(y))),\n mask\n );\n return true;\n }\n\n movePointer(x: number, y: number): boolean {\n return this.#pointer(x, y, 0);\n }\n\n clickPointer(x: number, y: number, button: PointerButton = 0): boolean {\n if (!this.#pointer(x, y, 0)) return false;\n this.#pointer(x, y, BUTTON_MASK[button]);\n this.#pointer(x, y, 0);\n return true;\n }\n\n pointerButton(x: number, y: number, button: PointerButton = 0, pressed = true): boolean {\n return this.#pointer(x, y, pressed ? BUTTON_MASK[button] : 0);\n }\n\n scrollPointer(x: number, y: number, delta: { x: number; y: number }): boolean {\n // RFB represents wheels as buttons: 4/5 are up/down and 6/7 are left/right.\n // The tool boundary already limits each axis, but keep the wire path bounded\n // too because other callers may be added later.\n for (const mask of vncWheelMasks(delta)) {\n if (!this.#pointer(x, y, mask)) return false;\n if (!this.#pointer(x, y, 0)) return false;\n }\n return true;\n }\n\n /**\n * VNC has no Unicode input message, so text is typed as keysyms. For every\n * character in the Basic Multilingual Plane the keysym is the code point.\n */\n typeText(text: string): boolean {\n if (this.#rfb._rfbConnectionState !== 'connected' || this.#rfb.viewOnly || !text) return false;\n\n for (const character of text) {\n const point = character.codePointAt(0) ?? 0;\n const keysym = character === '\\n' ? 0xff0d : character === '\\t' ? 0xff09 : point;\n this.#rfb.sendKey(keysym, null, true);\n this.#rfb.sendKey(keysym, null, false);\n }\n return true;\n }\n}\n\n/**\n * Turn a close code and the gateway's reason into something a person can act\n * on. Returns null when nothing VNC-specific applies, leaving the shared text.\n */\nfunction describeDisconnect({ code, reason, suffix }: DisconnectContext): string | null {\n if (code === 4001 || /auth|password|credential/i.test(reason)) {\n return `VNC authentication failed${suffix}. Verify the macOS account username and its password.`;\n }\n if (code === 4002 || /apple dh/i.test(reason)) {\n return `macOS VNC authentication handshake failed${suffix}.`;\n }\n if (code === 1011 || /cannot connect|unreachable|tcp error|tcp write/i.test(reason)) {\n return `The Worker could not keep a TCP connection to the VNC host${suffix}. `\n + 'Check that Screen Sharing is running and listening on this address.';\n }\n return null;\n}\n\nexport const vncDriver: ProtocolDriver = {\n protocol: 'vnc',\n label: 'VNC Client',\n defaultPort: DEFAULT_PORTS.vnc,\n dialog: DIALOG,\n buildUrl,\n createSession: (container, url, options) => new VncSession(container, url, options),\n describeState: (session) => (\n session instanceof VncSession ? session.describeState() : {}\n ),\n describeDisconnect,\n};\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ DEFAULT_PORTS,
3
+ toPort,
4
+ toRdpSecurity,
5
+ toRemoteProtocol
6
+ } from "./protocol";
7
+ const DEFAULT_DISPLAY_SETTINGS = {
8
+ quality: 6,
9
+ compression: 2,
10
+ scaling: true,
11
+ viewOnly: false
12
+ };
13
+ function connectionId(input) {
14
+ const program = input.remoteApp?.program?.trim() ?? "";
15
+ const base = `${input.protocol || "vnc"}:${input.host.trim()}:${input.port || ""}`;
16
+ return (program ? `${base}:${program}` : base).toLowerCase().slice(0, 64);
17
+ }
18
+ function emptyConnection(protocol = "vnc") {
19
+ const port = DEFAULT_PORTS[protocol];
20
+ return {
21
+ id: "",
22
+ label: "",
23
+ protocol,
24
+ host: "",
25
+ port,
26
+ username: "",
27
+ password: "",
28
+ domain: "",
29
+ security: "auto",
30
+ remoteApp: null,
31
+ display: { ...DEFAULT_DISPLAY_SETTINGS },
32
+ ssh: { enabled: false, port: DEFAULT_PORTS.ssh, username: "" },
33
+ openSshOnConnect: false
34
+ };
35
+ }
36
+ function clampLevel(value, fallback, min, max) {
37
+ const level = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
38
+ if (!Number.isInteger(level)) return fallback;
39
+ return Math.min(max, Math.max(min, level));
40
+ }
41
+ function normalizeDisplaySettings(value) {
42
+ const input = value ?? {};
43
+ return {
44
+ quality: clampLevel(input.quality, DEFAULT_DISPLAY_SETTINGS.quality, 1, 9),
45
+ compression: clampLevel(input.compression, DEFAULT_DISPLAY_SETTINGS.compression, 0, 9),
46
+ scaling: typeof input.scaling === "boolean" ? input.scaling : DEFAULT_DISPLAY_SETTINGS.scaling,
47
+ viewOnly: Boolean(input.viewOnly)
48
+ };
49
+ }
50
+ function normalizeRemoteApp(value) {
51
+ if (typeof value === "string") {
52
+ const program2 = value.trim();
53
+ return program2 ? { program: program2 } : null;
54
+ }
55
+ if (!value || typeof value !== "object") return null;
56
+ const input = value;
57
+ const program = String(input.program ?? "").trim();
58
+ if (!program) return null;
59
+ const workingDir = String(input.workingDir ?? "").trim();
60
+ const args = String(input.arguments ?? "").trim();
61
+ return {
62
+ program,
63
+ ...workingDir ? { workingDir } : {},
64
+ ...args ? { arguments: args } : {}
65
+ };
66
+ }
67
+ function normalizeConnection(input) {
68
+ const protocol = toRemoteProtocol(input.protocol);
69
+ const host = String(input.host ?? "").trim();
70
+ const port = toPort(input.port, DEFAULT_PORTS[protocol]);
71
+ const remoteApp = normalizeRemoteApp(input.remoteApp ?? input.program);
72
+ const sshInput = input.ssh ?? {};
73
+ const ssh = {
74
+ enabled: Boolean(sshInput.enabled),
75
+ port: toPort(sshInput.port, DEFAULT_PORTS.ssh),
76
+ username: String(sshInput.username ?? "").trim()
77
+ };
78
+ const settings = {
79
+ id: "",
80
+ label: String(input.label ?? "").trim() || host,
81
+ protocol,
82
+ host,
83
+ port,
84
+ username: String(input.username ?? "").trim(),
85
+ password: String(input.password ?? ""),
86
+ domain: String(input.domain ?? "").trim(),
87
+ security: toRdpSecurity(input.security),
88
+ remoteApp,
89
+ display: normalizeDisplaySettings(input.display),
90
+ ssh,
91
+ openSshOnConnect: Boolean(input.openSshOnConnect)
92
+ };
93
+ settings.id = String(input.id ?? "").trim() || connectionId(settings);
94
+ return settings;
95
+ }
96
+ function withoutPassword(settings) {
97
+ const { password: _password, ...rest } = settings;
98
+ return rest;
99
+ }
100
+ function describeConnection(settings) {
101
+ const target = `${settings.host || "no address"}:${settings.port}`;
102
+ const app = settings.remoteApp ? ` \xB7 ${settings.remoteApp.program}` : "";
103
+ return `${settings.protocol.toUpperCase()} \xB7 ${target}${app}`;
104
+ }
105
+ export {
106
+ DEFAULT_DISPLAY_SETTINGS,
107
+ connectionId,
108
+ describeConnection,
109
+ emptyConnection,
110
+ normalizeConnection,
111
+ normalizeDisplaySettings,
112
+ withoutPassword
113
+ };
114
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/connection.ts"],
4
+ "sourcesContent": ["/**\n * One connection, described once.\n *\n * Every way of starting a session -- a saved card on the welcome screen, an\n * address typed by hand, a RemoteApp launcher, a reconnect after a drop --\n * produces exactly this shape and hands it to exactly one dialog. That is the\n * whole point of the type: before it existed, a saved card could only ask for a\n * username and a password, so its port, security layer, domain and picture\n * quality were unreachable once the card had been made.\n */\n\nimport {\n DEFAULT_PORTS,\n toPort,\n toRdpSecurity,\n toRemoteProtocol,\n type RdpSecurity,\n type RemoteProtocol,\n} from './protocol';\n\n/** A published Windows application launched instead of the full desktop. */\nexport interface RemoteAppLaunch {\n /** Alias (`||notepad`) or an executable path. */\n program: string;\n workingDir?: string;\n arguments?: string;\n}\n\n/** The companion terminal offered beside a desktop. */\nexport interface SshCompanion {\n enabled: boolean;\n port: number;\n username: string;\n}\n\n/** Picture and input options. VNC negotiates them; RDP and SSH ignore what does not apply. */\nexport interface DisplaySettings {\n /** RFB quality level, 1 (smallest) to 9 (sharpest). */\n quality: number;\n /** RFB compression level, 0 (fastest) to 9 (smallest). */\n compression: number;\n /** Fit the remote desktop to the pane rather than scrolling it. */\n scaling: boolean;\n /** Watch without sending keyboard or pointer input. */\n viewOnly: boolean;\n}\n\n/**\n * Everything a session needs. `password` is the only field never persisted\n * anywhere -- not in the device library on the gateway, not in localStorage.\n */\nexport interface ConnectionSettings {\n /** Stable identity: protocol, address, port and program. See {@link connectionId}. */\n id: string;\n label: string;\n\n protocol: RemoteProtocol;\n host: string;\n port: number;\n\n username: string;\n password: string;\n /** Windows domain or computer name. RDP only. */\n domain: string;\n\n security: RdpSecurity;\n remoteApp: RemoteAppLaunch | null;\n\n display: DisplaySettings;\n ssh: SshCompanion;\n /** Open the terminal panel as soon as the desktop appears. */\n openSshOnConnect: boolean;\n}\n\nexport const DEFAULT_DISPLAY_SETTINGS: DisplaySettings = {\n quality: 6,\n compression: 2,\n scaling: true,\n viewOnly: false,\n};\n\n/**\n * A connection's identity is what it points at, not when it was made. Editing\n * the address therefore makes a different connection rather than silently\n * repointing an existing card, which is the behaviour people expect from a\n * list of machines.\n */\nexport function connectionId(\n input: Pick<ConnectionSettings, 'protocol' | 'host' | 'port'> & { remoteApp?: RemoteAppLaunch | null }\n): string {\n const program = input.remoteApp?.program?.trim() ?? '';\n const base = `${input.protocol || 'vnc'}:${input.host.trim()}:${input.port || ''}`;\n // This format is load-bearing, not arbitrary: device libraries already on\n // disk are keyed by it. Change it and every saved machine turns into a\n // second, empty card the first time someone connects to it again.\n return (program ? `${base}:${program}` : base).toLowerCase().slice(0, 64);\n}\n\n/** A blank connection, ready for the dialog to fill in. */\nexport function emptyConnection(protocol: RemoteProtocol = 'vnc'): ConnectionSettings {\n const port = DEFAULT_PORTS[protocol];\n return {\n id: '',\n label: '',\n protocol,\n host: '',\n port,\n username: '',\n password: '',\n domain: '',\n security: 'auto',\n remoteApp: null,\n display: { ...DEFAULT_DISPLAY_SETTINGS },\n ssh: { enabled: false, port: DEFAULT_PORTS.ssh, username: '' },\n openSshOnConnect: false,\n };\n}\n\nfunction clampLevel(value: unknown, fallback: number, min: number, max: number): number {\n const level = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);\n if (!Number.isInteger(level)) return fallback;\n return Math.min(max, Math.max(min, level));\n}\n\nexport function normalizeDisplaySettings(value: unknown): DisplaySettings {\n const input = (value ?? {}) as Partial<DisplaySettings>;\n return {\n quality: clampLevel(input.quality, DEFAULT_DISPLAY_SETTINGS.quality, 1, 9),\n compression: clampLevel(input.compression, DEFAULT_DISPLAY_SETTINGS.compression, 0, 9),\n scaling: typeof input.scaling === 'boolean' ? input.scaling : DEFAULT_DISPLAY_SETTINGS.scaling,\n viewOnly: Boolean(input.viewOnly),\n };\n}\n\nfunction normalizeRemoteApp(value: unknown): RemoteAppLaunch | null {\n if (typeof value === 'string') {\n const program = value.trim();\n return program ? { program } : null;\n }\n if (!value || typeof value !== 'object') return null;\n\n const input = value as Record<string, unknown>;\n const program = String(input.program ?? '').trim();\n if (!program) return null;\n\n const workingDir = String(input.workingDir ?? '').trim();\n const args = String(input.arguments ?? '').trim();\n return {\n program,\n ...(workingDir ? { workingDir } : {}),\n ...(args ? { arguments: args } : {}),\n };\n}\n\n/**\n * Build a complete, valid connection out of whatever a caller happens to have.\n * Partial input from a saved card, a host list entry, a URL or localStorage all\n * arrive here, so the dialog never has to reason about missing fields.\n */\nexport function normalizeConnection(input: Partial<ConnectionSettings> & Record<string, unknown>): ConnectionSettings {\n const protocol = toRemoteProtocol(input.protocol);\n const host = String(input.host ?? '').trim();\n const port = toPort(input.port, DEFAULT_PORTS[protocol]);\n const remoteApp = normalizeRemoteApp(input.remoteApp ?? input.program);\n\n const sshInput = (input.ssh ?? {}) as Partial<SshCompanion>;\n const ssh: SshCompanion = {\n enabled: Boolean(sshInput.enabled),\n port: toPort(sshInput.port, DEFAULT_PORTS.ssh),\n username: String(sshInput.username ?? '').trim(),\n };\n\n const settings: ConnectionSettings = {\n id: '',\n label: String(input.label ?? '').trim() || host,\n protocol,\n host,\n port,\n username: String(input.username ?? '').trim(),\n password: String(input.password ?? ''),\n domain: String(input.domain ?? '').trim(),\n security: toRdpSecurity(input.security),\n remoteApp,\n display: normalizeDisplaySettings(input.display),\n ssh,\n openSshOnConnect: Boolean(input.openSshOnConnect),\n };\n\n settings.id = String(input.id ?? '').trim() || connectionId(settings);\n return settings;\n}\n\n/** What a connection is worth remembering by -- never the password. */\nexport type PersistedConnection = Omit<ConnectionSettings, 'password'>;\n\nexport function withoutPassword(settings: ConnectionSettings): PersistedConnection {\n const { password: _password, ...rest } = settings;\n return rest;\n}\n\n/** A one-line description for the dialog's summary row and the status bar. */\nexport function describeConnection(settings: ConnectionSettings): string {\n const target = `${settings.host || 'no address'}:${settings.port}`;\n const app = settings.remoteApp ? ` \u00B7 ${settings.remoteApp.program}` : '';\n return `${settings.protocol.toUpperCase()} \u00B7 ${target}${app}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["program"]
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ DEFAULT_PORTS,
3
+ DEFAULT_SSH_PORT,
4
+ isRdpSecurity,
5
+ toPort,
6
+ toRemoteProtocol
7
+ } from "./protocol";
8
+ function normalizeRemoteApp(value) {
9
+ if (typeof value === "string" && value.trim()) return { program: value.trim() };
10
+ if (!value || typeof value !== "object") return void 0;
11
+ const candidate = value;
12
+ const program = String(candidate.program ?? "").trim();
13
+ if (!program) return void 0;
14
+ return {
15
+ program,
16
+ ...candidate.workingDir ? { workingDir: String(candidate.workingDir) } : {},
17
+ ...candidate.arguments ? { arguments: String(candidate.arguments) } : {}
18
+ };
19
+ }
20
+ function normalizeSsh(value, fallbackUsername) {
21
+ const inherited = typeof fallbackUsername === "string" && fallbackUsername.trim() ? fallbackUsername.trim() : void 0;
22
+ if (value === true) {
23
+ return { port: DEFAULT_SSH_PORT, ...inherited ? { username: inherited } : {} };
24
+ }
25
+ if (typeof value === "number" || typeof value === "string" && /^\d+$/.test(value.trim())) {
26
+ const port = toPort(value, 0);
27
+ if (!port) return void 0;
28
+ return { port, ...inherited ? { username: inherited } : {} };
29
+ }
30
+ if (!value || typeof value !== "object") return void 0;
31
+ const candidate = value;
32
+ if (candidate.enabled === false) return void 0;
33
+ const username = typeof candidate.username === "string" && candidate.username.trim() ? candidate.username.trim() : inherited;
34
+ return {
35
+ port: toPort(candidate.port, DEFAULT_SSH_PORT),
36
+ ...username ? { username } : {}
37
+ };
38
+ }
39
+ function parseHostList(hostsJson, defaultProtocol, defaultHost, defaultPort, idPrefix) {
40
+ if (!hostsJson) return [];
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(hostsJson);
44
+ } catch (error) {
45
+ console.warn(`Failed to parse ${idPrefix.toUpperCase()}_HOSTS JSON, ignoring it:`, error);
46
+ return [];
47
+ }
48
+ if (!Array.isArray(parsed) || parsed.length === 0) return [];
49
+ return parsed.map((raw, index) => {
50
+ const entry = raw ?? {};
51
+ const protocol = toRemoteProtocol(entry.protocol, defaultProtocol);
52
+ const remoteApp = protocol === "rdp" ? normalizeRemoteApp(entry.remoteApp) : void 0;
53
+ const ssh = normalizeSsh(entry.ssh ?? entry.sshPort, entry.sshUser ?? entry.sshUsername);
54
+ const host = String(entry.host ?? "") || defaultHost;
55
+ const name = String(entry.name ?? "").trim() || host;
56
+ return {
57
+ id: String(entry.id ?? "") || `${idPrefix}-${index + 1}`,
58
+ name,
59
+ host,
60
+ port: toPort(entry.port ?? defaultPort, DEFAULT_PORTS[protocol]),
61
+ protocol,
62
+ ...entry.domain ? { domain: String(entry.domain) } : {},
63
+ ...isRdpSecurity(entry.security) ? { security: entry.security } : {},
64
+ ...remoteApp ? { remoteApp } : {},
65
+ ...ssh ? { ssh } : {}
66
+ };
67
+ });
68
+ }
69
+ function parseConfiguredHosts(options) {
70
+ const defaultHost = options.defaultHost || "10.0.0.10";
71
+ const defaultPort = options.defaultPort || String(DEFAULT_PORTS.vnc);
72
+ const hosts = [
73
+ ...parseHostList(options.vncHostsJson, "vnc", defaultHost, defaultPort, "vnc-host"),
74
+ ...parseHostList(
75
+ options.rdpHostsJson,
76
+ "rdp",
77
+ options.defaultRdpHost || defaultHost,
78
+ options.defaultRdpPort || String(DEFAULT_PORTS.rdp),
79
+ "rdp-host"
80
+ )
81
+ ];
82
+ if (hosts.length > 0) return hosts;
83
+ return [
84
+ {
85
+ id: "default-host",
86
+ name: "Primary Host",
87
+ host: defaultHost,
88
+ port: toPort(defaultPort, DEFAULT_PORTS.vnc),
89
+ protocol: "vnc"
90
+ }
91
+ ];
92
+ }
93
+ function ipv4ToInt(value) {
94
+ const parts = String(value).trim().split(".");
95
+ if (parts.length !== 4) return null;
96
+ let result = 0;
97
+ for (const part of parts) {
98
+ if (!/^\d{1,3}$/.test(part)) return null;
99
+ const octet = Number.parseInt(part, 10);
100
+ if (octet > 255) return null;
101
+ result = result << 8 | octet;
102
+ }
103
+ return result >>> 0;
104
+ }
105
+ function parseAllowedSubnets(value) {
106
+ if (!value) return [];
107
+ const subnets = [];
108
+ for (const entry of value.split(",")) {
109
+ const text = entry.trim();
110
+ if (!text) continue;
111
+ const wildcard = text.split(".").findIndex((part) => part === "*");
112
+ const [address, bits] = wildcard >= 0 ? [text.replaceAll("*", "0"), String(wildcard * 8)] : text.split("/");
113
+ const base = ipv4ToInt(address ?? "");
114
+ if (base === null) continue;
115
+ const width = bits === void 0 ? 32 : Number.parseInt(bits, 10);
116
+ if (!Number.isInteger(width) || width < 0 || width > 32) continue;
117
+ const mask = width === 0 ? 0 : 4294967295 << 32 - width >>> 0;
118
+ subnets.push({ base: (base & mask) >>> 0, mask });
119
+ }
120
+ return subnets;
121
+ }
122
+ function hostInAllowedSubnets(host, value) {
123
+ const address = ipv4ToInt(host);
124
+ if (address === null) return false;
125
+ return parseAllowedSubnets(value).some(({ base, mask }) => (address & mask) >>> 0 === base);
126
+ }
127
+ export {
128
+ hostInAllowedSubnets,
129
+ ipv4ToInt,
130
+ parseAllowedSubnets,
131
+ parseConfiguredHosts
132
+ };
133
+ //# sourceMappingURL=hosts.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/hosts.ts"],
4
+ "sourcesContent": ["/**\n * The gateway's own roster of machines, and the networks it is allowed to dial.\n *\n * Parsing lives here, with no reference to the Worker environment, so the same\n * code shapes the list in `VNC_HOSTS`/`RDP_HOSTS` and the list the browser\n * receives from `/api/hosts`. Reading those variables is the Worker's job --\n * see `src/worker/config/hosts.ts`.\n */\n\nimport type { RemoteAppLaunch } from './connection';\nimport {\n DEFAULT_PORTS,\n DEFAULT_SSH_PORT,\n isRdpSecurity,\n toPort,\n toRemoteProtocol,\n type RdpSecurity,\n type RemoteProtocol,\n} from './protocol';\n\n/** Optional SSH access to the same machine, which publishes a terminal for it. */\nexport interface HostSshAccess {\n port: number;\n username?: string;\n}\n\n/** One machine as the gateway's configuration describes it. */\nexport interface RemoteHostTarget {\n id: string;\n name: string;\n host: string;\n port: number;\n protocol: RemoteProtocol;\n /** Windows domain or computer name pre-filled in the connect dialog. */\n domain?: string;\n security?: RdpSecurity;\n remoteApp?: RemoteAppLaunch;\n ssh?: HostSshAccess;\n}\n\n/** Raw JSON entries are whatever the operator typed; nothing is assumed. */\ntype RawHostEntry = Record<string, unknown>;\n\nfunction normalizeRemoteApp(value: unknown): RemoteAppLaunch | undefined {\n if (typeof value === 'string' && value.trim()) return { program: value.trim() };\n if (!value || typeof value !== 'object') return undefined;\n\n const candidate = value as RawHostEntry;\n const program = String(candidate.program ?? '').trim();\n if (!program) return undefined;\n\n return {\n program,\n ...(candidate.workingDir ? { workingDir: String(candidate.workingDir) } : {}),\n ...(candidate.arguments ? { arguments: String(candidate.arguments) } : {}),\n };\n}\n\n/**\n * `ssh` accepts `true`, a port number, or an object. Anything else means the\n * machine has no terminal to offer and no panel is shown for it.\n */\nfunction normalizeSsh(value: unknown, fallbackUsername: unknown): HostSshAccess | undefined {\n const inherited = typeof fallbackUsername === 'string' && fallbackUsername.trim()\n ? fallbackUsername.trim()\n : undefined;\n\n if (value === true) {\n return { port: DEFAULT_SSH_PORT, ...(inherited ? { username: inherited } : {}) };\n }\n\n if (typeof value === 'number' || (typeof value === 'string' && /^\\d+$/.test(value.trim()))) {\n const port = toPort(value, 0);\n if (!port) return undefined;\n return { port, ...(inherited ? { username: inherited } : {}) };\n }\n\n if (!value || typeof value !== 'object') return undefined;\n\n const candidate = value as RawHostEntry;\n if (candidate.enabled === false) return undefined;\n\n const username = typeof candidate.username === 'string' && candidate.username.trim()\n ? candidate.username.trim()\n : inherited;\n\n return {\n port: toPort(candidate.port, DEFAULT_SSH_PORT),\n ...(username ? { username } : {}),\n };\n}\n\nfunction parseHostList(\n hostsJson: string | undefined,\n defaultProtocol: RemoteProtocol,\n defaultHost: string,\n defaultPort: string,\n idPrefix: string\n): RemoteHostTarget[] {\n if (!hostsJson) return [];\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(hostsJson);\n } catch (error) {\n console.warn(`Failed to parse ${idPrefix.toUpperCase()}_HOSTS JSON, ignoring it:`, error);\n return [];\n }\n\n if (!Array.isArray(parsed) || parsed.length === 0) return [];\n\n return parsed.map((raw: unknown, index): RemoteHostTarget => {\n const entry = (raw ?? {}) as RawHostEntry;\n const protocol = toRemoteProtocol(entry.protocol, defaultProtocol);\n const remoteApp = protocol === 'rdp' ? normalizeRemoteApp(entry.remoteApp) : undefined;\n const ssh = normalizeSsh(entry.ssh ?? entry.sshPort, entry.sshUser ?? entry.sshUsername);\n\n const host = String(entry.host ?? '') || defaultHost;\n const name = String(entry.name ?? '').trim() || host;\n\n return {\n id: String(entry.id ?? '') || `${idPrefix}-${index + 1}`,\n name,\n host,\n port: toPort(entry.port ?? defaultPort, DEFAULT_PORTS[protocol]),\n protocol,\n ...(entry.domain ? { domain: String(entry.domain) } : {}),\n ...(isRdpSecurity(entry.security) ? { security: entry.security } : {}),\n ...(remoteApp ? { remoteApp } : {}),\n ...(ssh ? { ssh } : {}),\n };\n });\n}\n\n/**\n * Build the target list offered in the connect dialog. `VNC_HOSTS` and\n * `RDP_HOSTS` are merged into one list; each entry may override its protocol,\n * so a single list can carry both kinds of target.\n */\nexport function parseConfiguredHosts(options: {\n vncHostsJson?: string;\n rdpHostsJson?: string;\n defaultHost?: string;\n defaultPort?: string;\n defaultRdpHost?: string;\n defaultRdpPort?: string;\n}): RemoteHostTarget[] {\n const defaultHost = options.defaultHost || '10.0.0.10';\n const defaultPort = options.defaultPort || String(DEFAULT_PORTS.vnc);\n\n const hosts = [\n ...parseHostList(options.vncHostsJson, 'vnc', defaultHost, defaultPort, 'vnc-host'),\n ...parseHostList(\n options.rdpHostsJson,\n 'rdp',\n options.defaultRdpHost || defaultHost,\n options.defaultRdpPort || String(DEFAULT_PORTS.rdp),\n 'rdp-host'\n ),\n ];\n\n if (hosts.length > 0) return hosts;\n\n return [\n {\n id: 'default-host',\n name: 'Primary Host',\n host: defaultHost,\n port: toPort(defaultPort, DEFAULT_PORTS.vnc),\n protocol: 'vnc',\n },\n ];\n}\n\n/** One allowed network, as a base address and a mask. */\nexport interface Subnet {\n base: number;\n mask: number;\n}\n\n/** Dotted-quad to a 32-bit number, or null for anything that is not one. */\nexport function ipv4ToInt(value: string): number | null {\n const parts = String(value).trim().split('.');\n if (parts.length !== 4) return null;\n\n let result = 0;\n for (const part of parts) {\n if (!/^\\d{1,3}$/.test(part)) return null;\n const octet = Number.parseInt(part, 10);\n if (octet > 255) return null;\n result = (result << 8) | octet;\n }\n return result >>> 0;\n}\n\n/**\n * The networks this gateway may dial, beyond the machines named in the host\n * lists. Written as `ALLOWED_SUBNETS`, comma-separated:\n *\n * 192.168.8.0/24 a CIDR block\n * 192.168.8.* the same thing, written the way people say it\n * 192.168.8.67 a single address\n *\n * This is what lets a terminal open on a machine that was never listed. Keep it\n * to networks you own: anything inside one can be reached on any port by anyone\n * who gets past the gateway's login.\n */\nexport function parseAllowedSubnets(value?: string): Subnet[] {\n if (!value) return [];\n\n const subnets: Subnet[] = [];\n for (const entry of value.split(',')) {\n const text = entry.trim();\n if (!text) continue;\n\n // `192.168.8.*` is the same shape as `192.168.8.0/24`, one octet per step.\n const wildcard = text.split('.').findIndex((part) => part === '*');\n const [address, bits] = wildcard >= 0\n ? [text.replaceAll('*', '0'), String(wildcard * 8)]\n : text.split('/');\n\n const base = ipv4ToInt(address ?? '');\n if (base === null) continue;\n\n const width = bits === undefined ? 32 : Number.parseInt(bits, 10);\n if (!Number.isInteger(width) || width < 0 || width > 32) continue;\n\n // A /0 would shift by 32, which JavaScript wraps back to no shift at all.\n const mask = width === 0 ? 0 : (0xffffffff << (32 - width)) >>> 0;\n subnets.push({ base: (base & mask) >>> 0, mask });\n }\n return subnets;\n}\n\n/** Whether a dotted-quad address falls inside one of the allowed networks. */\nexport function hostInAllowedSubnets(host: string, value?: string): boolean {\n const address = ipv4ToInt(host);\n if (address === null) return false;\n return parseAllowedSubnets(value).some(({ base, mask }) => ((address & mask) >>> 0) === base);\n}\n"],
5
+ "mappings": "AAUA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAyBP,SAAS,mBAAmB,OAA6C;AACvE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAG,QAAO,EAAE,SAAS,MAAM,KAAK,EAAE;AAC9E,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAEhD,QAAM,YAAY;AAClB,QAAM,UAAU,OAAO,UAAU,WAAW,EAAE,EAAE,KAAK;AACrD,MAAI,CAAC,QAAS,QAAO;AAErB,SAAO;AAAA,IACL;AAAA,IACA,GAAI,UAAU,aAAa,EAAE,YAAY,OAAO,UAAU,UAAU,EAAE,IAAI,CAAC;AAAA,IAC3E,GAAI,UAAU,YAAY,EAAE,WAAW,OAAO,UAAU,SAAS,EAAE,IAAI,CAAC;AAAA,EAC1E;AACF;AAMA,SAAS,aAAa,OAAgB,kBAAsD;AAC1F,QAAM,YAAY,OAAO,qBAAqB,YAAY,iBAAiB,KAAK,IAC5E,iBAAiB,KAAK,IACtB;AAEJ,MAAI,UAAU,MAAM;AAClB,WAAO,EAAE,MAAM,kBAAkB,GAAI,YAAY,EAAE,UAAU,UAAU,IAAI,CAAC,EAAG;AAAA,EACjF;AAEA,MAAI,OAAO,UAAU,YAAa,OAAO,UAAU,YAAY,QAAQ,KAAK,MAAM,KAAK,CAAC,GAAI;AAC1F,UAAM,OAAO,OAAO,OAAO,CAAC;AAC5B,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,EAAE,MAAM,GAAI,YAAY,EAAE,UAAU,UAAU,IAAI,CAAC,EAAG;AAAA,EAC/D;AAEA,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAEhD,QAAM,YAAY;AAClB,MAAI,UAAU,YAAY,MAAO,QAAO;AAExC,QAAM,WAAW,OAAO,UAAU,aAAa,YAAY,UAAU,SAAS,KAAK,IAC/E,UAAU,SAAS,KAAK,IACxB;AAEJ,SAAO;AAAA,IACL,MAAM,OAAO,UAAU,MAAM,gBAAgB;AAAA,IAC7C,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,EACjC;AACF;AAEA,SAAS,cACP,WACA,iBACA,aACA,aACA,UACoB;AACpB,MAAI,CAAC,UAAW,QAAO,CAAC;AAExB,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,SAAS;AAAA,EAC/B,SAAS,OAAO;AACd,YAAQ,KAAK,mBAAmB,SAAS,YAAY,CAAC,6BAA6B,KAAK;AACxF,WAAO,CAAC;AAAA,EACV;AAEA,MAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAG,QAAO,CAAC;AAE3D,SAAO,OAAO,IAAI,CAAC,KAAc,UAA4B;AAC3D,UAAM,QAAS,OAAO,CAAC;AACvB,UAAM,WAAW,iBAAiB,MAAM,UAAU,eAAe;AACjE,UAAM,YAAY,aAAa,QAAQ,mBAAmB,MAAM,SAAS,IAAI;AAC7E,UAAM,MAAM,aAAa,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW,MAAM,WAAW;AAEvF,UAAM,OAAO,OAAO,MAAM,QAAQ,EAAE,KAAK;AACzC,UAAM,OAAO,OAAO,MAAM,QAAQ,EAAE,EAAE,KAAK,KAAK;AAEhD,WAAO;AAAA,MACL,IAAI,OAAO,MAAM,MAAM,EAAE,KAAK,GAAG,QAAQ,IAAI,QAAQ,CAAC;AAAA,MACtD;AAAA,MACA;AAAA,MACA,MAAM,OAAO,MAAM,QAAQ,aAAa,cAAc,QAAQ,CAAC;AAAA,MAC/D;AAAA,MACA,GAAI,MAAM,SAAS,EAAE,QAAQ,OAAO,MAAM,MAAM,EAAE,IAAI,CAAC;AAAA,MACvD,GAAI,cAAc,MAAM,QAAQ,IAAI,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;AAAA,MACpE,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,MACjC,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,IACvB;AAAA,EACF,CAAC;AACH;AAOO,SAAS,qBAAqB,SAOd;AACrB,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,cAAc,QAAQ,eAAe,OAAO,cAAc,GAAG;AAEnE,QAAM,QAAQ;AAAA,IACZ,GAAG,cAAc,QAAQ,cAAc,OAAO,aAAa,aAAa,UAAU;AAAA,IAClF,GAAG;AAAA,MACD,QAAQ;AAAA,MACR;AAAA,MACA,QAAQ,kBAAkB;AAAA,MAC1B,QAAQ,kBAAkB,OAAO,cAAc,GAAG;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,MAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,SAAO;AAAA,IACL;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM,OAAO,aAAa,cAAc,GAAG;AAAA,MAC3C,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AASO,SAAS,UAAU,OAA8B;AACtD,QAAM,QAAQ,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG;AAC5C,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,YAAY,KAAK,IAAI,EAAG,QAAO;AACpC,UAAM,QAAQ,OAAO,SAAS,MAAM,EAAE;AACtC,QAAI,QAAQ,IAAK,QAAO;AACxB,aAAU,UAAU,IAAK;AAAA,EAC3B;AACA,SAAO,WAAW;AACpB;AAcO,SAAS,oBAAoB,OAA0B;AAC5D,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,QAAM,UAAoB,CAAC;AAC3B,aAAW,SAAS,MAAM,MAAM,GAAG,GAAG;AACpC,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,CAAC,KAAM;AAGX,UAAM,WAAW,KAAK,MAAM,GAAG,EAAE,UAAU,CAAC,SAAS,SAAS,GAAG;AACjE,UAAM,CAAC,SAAS,IAAI,IAAI,YAAY,IAChC,CAAC,KAAK,WAAW,KAAK,GAAG,GAAG,OAAO,WAAW,CAAC,CAAC,IAChD,KAAK,MAAM,GAAG;AAElB,UAAM,OAAO,UAAU,WAAW,EAAE;AACpC,QAAI,SAAS,KAAM;AAEnB,UAAM,QAAQ,SAAS,SAAY,KAAK,OAAO,SAAS,MAAM,EAAE;AAChE,QAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,GAAI;AAGzD,UAAM,OAAO,UAAU,IAAI,IAAK,cAAe,KAAK,UAAY;AAChE,YAAQ,KAAK,EAAE,OAAO,OAAO,UAAU,GAAG,KAAK,CAAC;AAAA,EAClD;AACA,SAAO;AACT;AAGO,SAAS,qBAAqB,MAAc,OAAyB;AAC1E,QAAM,UAAU,UAAU,IAAI;AAC9B,MAAI,YAAY,KAAM,QAAO;AAC7B,SAAO,oBAAoB,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAQ,UAAU,UAAU,MAAO,IAAI;AAC9F;",
6
+ "names": []
7
+ }
@@ -0,0 +1,42 @@
1
+ const ICON_PATHS = {
2
+ monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
3
+ link: '<path d="M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 1 1 0 10h-2M8 12h8"/>',
4
+ power: '<path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.8 0"/>',
5
+ maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3"/>',
6
+ minimize: '<path d="M8 3v3a2 2 0 0 1-2 2H3M21 8h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3M16 21v-3a2 2 0 0 1 2-2h3"/>',
7
+ keyboard: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/>',
8
+ clipboard: '<rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>',
9
+ check: '<path d="m5 13 4 4 10-10"/>',
10
+ camera: '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>',
11
+ eye: '<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>',
12
+ sliders: '<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M2 14h4M10 8h4M18 16h4"/>',
13
+ expand: '<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>',
14
+ shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
15
+ chevron: '<path d="m6 9 6 6 6-6"/>',
16
+ back: '<path d="m15 18-6-6 6-6"/>',
17
+ zap: '<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>',
18
+ cube: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m3.3 7 8.7 5 8.7-5M12 22V12"/>',
19
+ logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/>',
20
+ clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/>',
21
+ activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
22
+ transfer: '<path d="m3 16 4 4 4-4M7 20V4M21 8l-4-4-4 4M17 4v16"/>',
23
+ screen: '<rect x="2" y="5" width="20" height="13" rx="2"/><path d="M7 21h10"/>',
24
+ command: '<path d="M15 6a3 3 0 1 1 3 3h-3zm0 0v12m0 0a3 3 0 1 0 3-3h-3zM9 6a3 3 0 1 0-3 3h3zm0 0v12m0 0a3 3 0 1 1-3-3h3z"/>',
25
+ apps: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
26
+ terminal: '<path d="m4 17 6-6-6-6M12 19h8"/>',
27
+ folder: '<path d="M3 7h6l2 2h10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M3 7V6a2 2 0 0 1 2-2h4l2 3"/>',
28
+ close: '<path d="M18 6 6 18M6 6l12 12"/>',
29
+ sparkles: '<path d="m12 3 1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z"/><path d="m19 15 .8 2.2 2.2.8-2.2.8L19 21l-.8-2.2-2.2-.8 2.2-.8z"/>',
30
+ broom: '<path d="M10 10V4a2 2 0 0 1 4 0v6"/><path d="M6 10h12a2 2 0 0 1 2 2v2H4v-2a2 2 0 0 1 2-2Z"/><path d="m5 14-2 7h18l-2-7M9 18l-1 3M15 18l1 3"/>',
31
+ panel: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/>',
32
+ menu: '<path d="M4 7h16M4 12h16M4 17h16"/>',
33
+ plus: '<path d="M12 5v14M5 12h14"/>',
34
+ star: '<path d="m12 3 2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.8 6.2 20.9l1.1-6.5L2.6 9.8l6.5-.9L12 3z"/>',
35
+ trash: '<path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>'
36
+ };
37
+ const FAVICON_DATA_URI = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f38020' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='14' rx='2'/%3E%3Cpath d='M8 21h8M12 17v4'/%3E%3C/svg%3E";
38
+ export {
39
+ FAVICON_DATA_URI,
40
+ ICON_PATHS
41
+ };
42
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/icons.ts"],
4
+ "sourcesContent": ["/**\n * Icon path data, shared by the server-rendered login page and the React app.\n *\n * Each value is the inside of a 24x24 stroked `<svg>`; the wrapper differs\n * between the two (a string on the Worker, a component in the browser) but the\n * geometry should not.\n */\n\nexport const ICON_PATHS = {\n monitor: '<rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\"/><path d=\"M8 21h8M12 17v4\"/>',\n link: '<path d=\"M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 1 1 0 10h-2M8 12h8\"/>',\n power: '<path d=\"M12 2v10\"/><path d=\"M18.4 6.6a9 9 0 1 1-12.8 0\"/>',\n maximize: '<path d=\"M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3\"/>',\n minimize: '<path d=\"M8 3v3a2 2 0 0 1-2 2H3M21 8h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3M16 21v-3a2 2 0 0 1 2-2h3\"/>',\n keyboard: '<rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\"/><path d=\"M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10\"/>',\n clipboard: '<rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/>',\n check: '<path d=\"m5 13 4 4 10-10\"/>',\n camera: '<path d=\"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z\"/><circle cx=\"12\" cy=\"13\" r=\"3\"/>',\n eye: '<path d=\"M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n sliders: '<path d=\"M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M2 14h4M10 8h4M18 16h4\"/>',\n expand: '<path d=\"M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7\"/>',\n shield: '<path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/>',\n chevron: '<path d=\"m6 9 6 6 6-6\"/>',\n back: '<path d=\"m15 18-6-6 6-6\"/>',\n zap: '<path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/>',\n cube: '<path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"/><path d=\"m3.3 7 8.7 5 8.7-5M12 22V12\"/>',\n logout: '<path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3.5 2\"/>',\n activity: '<path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"/>',\n transfer: '<path d=\"m3 16 4 4 4-4M7 20V4M21 8l-4-4-4 4M17 4v16\"/>',\n screen: '<rect x=\"2\" y=\"5\" width=\"20\" height=\"13\" rx=\"2\"/><path d=\"M7 21h10\"/>',\n command: '<path d=\"M15 6a3 3 0 1 1 3 3h-3zm0 0v12m0 0a3 3 0 1 0 3-3h-3zM9 6a3 3 0 1 0-3 3h3zm0 0v12m0 0a3 3 0 1 1-3-3h3z\"/>',\n apps: '<rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/>',\n terminal: '<path d=\"m4 17 6-6-6-6M12 19h8\"/>',\n folder: '<path d=\"M3 7h6l2 2h10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/><path d=\"M3 7V6a2 2 0 0 1 2-2h4l2 3\"/>',\n close: '<path d=\"M18 6 6 18M6 6l12 12\"/>',\n sparkles: '<path d=\"m12 3 1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z\"/><path d=\"m19 15 .8 2.2 2.2.8-2.2.8L19 21l-.8-2.2-2.2-.8 2.2-.8z\"/>',\n broom: '<path d=\"M10 10V4a2 2 0 0 1 4 0v6\"/><path d=\"M6 10h12a2 2 0 0 1 2 2v2H4v-2a2 2 0 0 1 2-2Z\"/><path d=\"m5 14-2 7h18l-2-7M9 18l-1 3M15 18l1 3\"/>',\n panel: '<rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M14 4v16\"/>',\n menu: '<path d=\"M4 7h16M4 12h16M4 17h16\"/>',\n plus: '<path d=\"M12 5v14M5 12h14\"/>',\n star: '<path d=\"m12 3 2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.8 6.2 20.9l1.1-6.5L2.6 9.8l6.5-.9L12 3z\"/>',\n trash: '<path d=\"M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/>',\n} as const;\n\nexport type IconName = keyof typeof ICON_PATHS;\n\n/** The tab icon: a monitor in Cloudflare orange. */\nexport const FAVICON_DATA_URI = \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f38020' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='14' rx='2'/%3E%3Cpath d='M8 21h8M12 17v4'/%3E%3C/svg%3E\";\n"],
5
+ "mappings": "AAQO,MAAM,aAAa;AAAA,EACxB,SAAS;AAAA,EACT,MAAM;AAAA,EACN,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AACT;AAKO,MAAM,mBAAmB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,40 @@
1
+ const REMOTE_PROTOCOLS = ["vnc", "rdp"];
2
+ const TRANSPORT_PROTOCOLS = ["vnc", "rdp", "ssh"];
3
+ const RDP_SECURITY_MODES = ["auto", "nla", "tls", "rdp"];
4
+ const DEFAULT_PORTS = {
5
+ vnc: 5900,
6
+ rdp: 3389,
7
+ ssh: 22
8
+ };
9
+ const DEFAULT_SSH_PORT = DEFAULT_PORTS.ssh;
10
+ function isRemoteProtocol(value) {
11
+ return REMOTE_PROTOCOLS.includes(value);
12
+ }
13
+ function isRdpSecurity(value) {
14
+ return RDP_SECURITY_MODES.includes(value);
15
+ }
16
+ function toRemoteProtocol(value, fallback = "vnc") {
17
+ const wanted = String(value ?? "").toLowerCase();
18
+ return isRemoteProtocol(wanted) ? wanted : fallback;
19
+ }
20
+ function toRdpSecurity(value, fallback = "auto") {
21
+ const wanted = String(value ?? "").toLowerCase();
22
+ return isRdpSecurity(wanted) ? wanted : fallback;
23
+ }
24
+ function toPort(value, fallback) {
25
+ const port = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
26
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : fallback;
27
+ }
28
+ export {
29
+ DEFAULT_PORTS,
30
+ DEFAULT_SSH_PORT,
31
+ RDP_SECURITY_MODES,
32
+ REMOTE_PROTOCOLS,
33
+ TRANSPORT_PROTOCOLS,
34
+ isRdpSecurity,
35
+ isRemoteProtocol,
36
+ toPort,
37
+ toRdpSecurity,
38
+ toRemoteProtocol
39
+ };
40
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/protocol.ts"],
4
+ "sourcesContent": ["/**\n * The vocabulary both halves of the gateway share.\n *\n * The Worker and the browser have to agree on what a \"protocol\" is, what ports\n * they default to and how an RDP security layer is named, so those live here\n * rather than being re-declared on each side and drifting apart.\n */\n\n/** A remote-desktop protocol the session workspace can display. */\nexport const REMOTE_PROTOCOLS = ['vnc', 'rdp'] as const;\nexport type RemoteProtocol = (typeof REMOTE_PROTOCOLS)[number];\n\n/** Every protocol the gateway relays, including the terminal side panel. */\nexport const TRANSPORT_PROTOCOLS = ['vnc', 'rdp', 'ssh'] as const;\nexport type TransportProtocol = (typeof TRANSPORT_PROTOCOLS)[number];\n\n/**\n * The RDP security layer. `auto` negotiates with the host, which is right for\n * anything that answers honestly; the rest are for hosts that do not.\n */\nexport const RDP_SECURITY_MODES = ['auto', 'nla', 'tls', 'rdp'] as const;\nexport type RdpSecurity = (typeof RDP_SECURITY_MODES)[number];\n\nexport const DEFAULT_PORTS: Record<TransportProtocol, number> = {\n vnc: 5900,\n rdp: 3389,\n ssh: 22,\n};\n\nexport const DEFAULT_SSH_PORT = DEFAULT_PORTS.ssh;\n\nexport function isRemoteProtocol(value: unknown): value is RemoteProtocol {\n return REMOTE_PROTOCOLS.includes(value as RemoteProtocol);\n}\n\nexport function isRdpSecurity(value: unknown): value is RdpSecurity {\n return RDP_SECURITY_MODES.includes(value as RdpSecurity);\n}\n\n/** Coerce anything to a protocol, falling back to what the caller opens on. */\nexport function toRemoteProtocol(value: unknown, fallback: RemoteProtocol = 'vnc'): RemoteProtocol {\n const wanted = String(value ?? '').toLowerCase();\n return isRemoteProtocol(wanted) ? wanted : fallback;\n}\n\nexport function toRdpSecurity(value: unknown, fallback: RdpSecurity = 'auto'): RdpSecurity {\n const wanted = String(value ?? '').toLowerCase();\n return isRdpSecurity(wanted) ? wanted : fallback;\n}\n\n/** A port number, or the protocol's default when the value is not one. */\nexport function toPort(value: unknown, fallback: number): number {\n const port = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);\n return Number.isInteger(port) && port > 0 && port < 65536 ? port : fallback;\n}\n"],
5
+ "mappings": "AASO,MAAM,mBAAmB,CAAC,OAAO,KAAK;AAItC,MAAM,sBAAsB,CAAC,OAAO,OAAO,KAAK;AAOhD,MAAM,qBAAqB,CAAC,QAAQ,OAAO,OAAO,KAAK;AAGvD,MAAM,gBAAmD;AAAA,EAC9D,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEO,MAAM,mBAAmB,cAAc;AAEvC,SAAS,iBAAiB,OAAyC;AACxE,SAAO,iBAAiB,SAAS,KAAuB;AAC1D;AAEO,SAAS,cAAc,OAAsC;AAClE,SAAO,mBAAmB,SAAS,KAAoB;AACzD;AAGO,SAAS,iBAAiB,OAAgB,WAA2B,OAAuB;AACjG,QAAM,SAAS,OAAO,SAAS,EAAE,EAAE,YAAY;AAC/C,SAAO,iBAAiB,MAAM,IAAI,SAAS;AAC7C;AAEO,SAAS,cAAc,OAAgB,WAAwB,QAAqB;AACzF,QAAM,SAAS,OAAO,SAAS,EAAE,EAAE,YAAY;AAC/C,SAAO,cAAc,MAAM,IAAI,SAAS;AAC1C;AAGO,SAAS,OAAO,OAAgB,UAA0B;AAC/D,QAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,SAAS,OAAO,SAAS,EAAE,GAAG,EAAE;AACxF,SAAO,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,QAAQ,OAAO;AACrE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,42 @@
1
+ const GATEWAY_READY_SIGNAL = "VNC_GATEWAY_READY_v1";
2
+ const GATEWAY_KEEPALIVE_SIGNAL = "VNC_GATEWAY_KEEPALIVE_v1";
3
+ const KEEPALIVE_INTERVAL_MS = 25e3;
4
+ const MAX_CLOSE_REASON_BYTES = 123;
5
+ function isRetryableDisconnect(diagnostic) {
6
+ if (!diagnostic.established || !diagnostic.close) return false;
7
+ if (diagnostic.close.code === 1006) return true;
8
+ return diagnostic.close.code === 1011 && /tcp|socket|network|host closed/i.test(diagnostic.close.reason);
9
+ }
10
+ function websocketCloseReason(reason) {
11
+ const encoder = new TextEncoder();
12
+ if (encoder.encode(reason).byteLength <= MAX_CLOSE_REASON_BYTES) return reason;
13
+ let shortened = "";
14
+ for (const character of reason) {
15
+ if (encoder.encode(`${shortened}${character}\u2026`).byteLength > MAX_CLOSE_REASON_BYTES) break;
16
+ shortened += character;
17
+ }
18
+ return `${shortened}\u2026`;
19
+ }
20
+ function sendableCloseCode(code) {
21
+ return code === 1005 || code === 1006 || code === 1015 ? 1011 : code;
22
+ }
23
+ async function messageToBytes(data) {
24
+ if (typeof data === "string") return new TextEncoder().encode(data);
25
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
26
+ if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
27
+ if (ArrayBuffer.isView(data)) {
28
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
29
+ }
30
+ return null;
31
+ }
32
+ export {
33
+ GATEWAY_KEEPALIVE_SIGNAL,
34
+ GATEWAY_READY_SIGNAL,
35
+ KEEPALIVE_INTERVAL_MS,
36
+ MAX_CLOSE_REASON_BYTES,
37
+ isRetryableDisconnect,
38
+ messageToBytes,
39
+ sendableCloseCode,
40
+ websocketCloseReason
41
+ };
42
+ //# sourceMappingURL=signals.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/signals.ts"],
4
+ "sourcesContent": ["/**\n * Control frames the browser and the gateway exchange on the WebSocket that\n * carries a remote-desktop stream.\n *\n * They are plain strings precisely so they can never be confused with the\n * binary protocol payload flowing in either direction, and both sides import\n * these same literals so the two halves cannot drift apart.\n */\n\n/** Browser -> gateway: \"my WebSocket is open, you may start reading the host\". */\nexport const GATEWAY_READY_SIGNAL = 'VNC_GATEWAY_READY_v1';\n\n/**\n * Browser -> gateway: keep the edge from idling out a silent session.\n * Cloudflare closes a WebSocket with no traffic in either direction for 100s,\n * and an untouched desktop transmits nothing at all.\n */\nexport const GATEWAY_KEEPALIVE_SIGNAL = 'VNC_GATEWAY_KEEPALIVE_v1';\n\n/**\n * How often the browser nudges the transport. Well inside the edge's 100s\n * window, so a session that is merely being looked at is never torn down\n * without a close frame -- which is what a bare WebSocket 1006 used to mean.\n */\nexport const KEEPALIVE_INTERVAL_MS = 25_000;\n\nexport const MAX_CLOSE_REASON_BYTES = 123;\n\n/** Whether a live desktop ended for a transient transport reason worth retrying. */\nexport function isRetryableDisconnect(diagnostic: {\n close: { code: number; reason: string } | null;\n established: boolean;\n}): boolean {\n if (!diagnostic.established || !diagnostic.close) return false;\n if (diagnostic.close.code === 1006) return true;\n return diagnostic.close.code === 1011\n && /tcp|socket|network|host closed/i.test(diagnostic.close.reason);\n}\n\n/** WebSocket close reasons are capped at 123 bytes; trim without splitting a code point. */\nexport function websocketCloseReason(reason: string): string {\n const encoder = new TextEncoder();\n if (encoder.encode(reason).byteLength <= MAX_CLOSE_REASON_BYTES) return reason;\n\n let shortened = '';\n for (const character of reason) {\n if (encoder.encode(`${shortened}${character}\u2026`).byteLength > MAX_CLOSE_REASON_BYTES) break;\n shortened += character;\n }\n return `${shortened}\u2026`;\n}\n\n/** 1005/1006/1015 are reserved and cannot be transmitted in a close frame. */\nexport function sendableCloseCode(code: number): number {\n return code === 1005 || code === 1006 || code === 1015 ? 1011 : code;\n}\n\n/** Normalize any WebSocket message payload to bytes. */\nexport async function messageToBytes(data: unknown): Promise<Uint8Array | null> {\n if (typeof data === 'string') return new TextEncoder().encode(data);\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());\n if (ArrayBuffer.isView(data)) {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n return null;\n}\n"],
5
+ "mappings": "AAUO,MAAM,uBAAuB;AAO7B,MAAM,2BAA2B;AAOjC,MAAM,wBAAwB;AAE9B,MAAM,yBAAyB;AAG/B,SAAS,sBAAsB,YAG1B;AACV,MAAI,CAAC,WAAW,eAAe,CAAC,WAAW,MAAO,QAAO;AACzD,MAAI,WAAW,MAAM,SAAS,KAAM,QAAO;AAC3C,SAAO,WAAW,MAAM,SAAS,QAC5B,kCAAkC,KAAK,WAAW,MAAM,MAAM;AACrE;AAGO,SAAS,qBAAqB,QAAwB;AAC3D,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,QAAQ,OAAO,MAAM,EAAE,cAAc,uBAAwB,QAAO;AAExE,MAAI,YAAY;AAChB,aAAW,aAAa,QAAQ;AAC9B,QAAI,QAAQ,OAAO,GAAG,SAAS,GAAG,SAAS,QAAG,EAAE,aAAa,uBAAwB;AACrF,iBAAa;AAAA,EACf;AACA,SAAO,GAAG,SAAS;AACrB;AAGO,SAAS,kBAAkB,MAAsB;AACtD,SAAO,SAAS,QAAQ,SAAS,QAAQ,SAAS,OAAO,OAAO;AAClE;AAGA,eAAsB,eAAe,MAA2C;AAC9E,MAAI,OAAO,SAAS,SAAU,QAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AAClE,MAAI,gBAAgB,YAAa,QAAO,IAAI,WAAW,IAAI;AAC3D,MAAI,gBAAgB,KAAM,QAAO,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AACxE,MAAI,YAAY,OAAO,IAAI,GAAG;AAC5B,WAAO,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AAAA,EACrE;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "ai-remote",
3
+ "version": "0.1.0",
4
+ "description": "Pure TypeScript protocol engines (RDP, VNC, SSH) and remote desktop vocabulary for AI agents, Node.js, and web browsers",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Leon Guo",
9
+ "email": "liang.x.guo@gmail.com",
10
+ "url": "https://github.com/guocity"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/guocity/vnc-node-worker.git"
15
+ },
16
+ "keywords": [
17
+ "remote-desktop",
18
+ "vnc",
19
+ "rdp",
20
+ "ssh",
21
+ "ai-agent",
22
+ "remote-control",
23
+ "novnc",
24
+ "xterm"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./src/index.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./src/index.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./src/index.ts"
33
+ },
34
+ "./shared/*": {
35
+ "types": "./src/shared/*.ts",
36
+ "import": "./dist/shared/*.js",
37
+ "default": "./src/shared/*.ts"
38
+ },
39
+ "./protocols": {
40
+ "types": "./src/protocols/index.ts",
41
+ "import": "./dist/protocols/index.js",
42
+ "default": "./src/protocols/index.ts"
43
+ },
44
+ "./protocols/*": {
45
+ "types": "./src/protocols/*.ts",
46
+ "import": "./dist/protocols/*.js",
47
+ "default": "./src/protocols/*.ts"
48
+ }
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "src",
53
+ "README.md"
54
+ ],
55
+ "scripts": {
56
+ "build": "node scripts/build.mjs",
57
+ "prepack": "node scripts/build.mjs"
58
+ },
59
+ "peerDependencies": {
60
+ "@novnc/novnc": "^1.7.0",
61
+ "@xterm/addon-fit": "^0.11.0",
62
+ "@xterm/xterm": "^6.0.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@novnc/novnc": {
66
+ "optional": true
67
+ },
68
+ "@xterm/addon-fit": {
69
+ "optional": true
70
+ },
71
+ "@xterm/xterm": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@novnc/novnc": "^1.7.0",
77
+ "@xterm/addon-fit": "^0.11.0",
78
+ "@xterm/xterm": "^6.0.0",
79
+ "esbuild": "^0.28.2",
80
+ "typescript": "^7.0.2"
81
+ }
82
+ }