@medplum/agent 2.0.30 → 2.0.32

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/installer.nsi ADDED
@@ -0,0 +1,224 @@
1
+ # Medplum Agent Installer Builder
2
+ # For use with NSIS 3.0+
3
+ # See: https://nsis.sourceforge.io/
4
+
5
+ !define COMPANY_NAME "Medplum"
6
+ !define APP_NAME "Medplum Agent"
7
+ !define SERVICE_NAME "MedplumAgent"
8
+ !define INSTALLER_FILE_NAME "medplum-agent-installer.exe"
9
+ !define DEFAULT_BASE_URL "https://api.medplum.com/"
10
+
11
+ Name "${APP_NAME}"
12
+ OutFile "${INSTALLER_FILE_NAME}"
13
+ VIProductVersion "1.0.0.0"
14
+ VIAddVersionKey ProductName "${APP_NAME}"
15
+ VIAddVersionKey Comments "${APP_NAME}"
16
+ VIAddVersionKey CompanyName "${COMPANY_NAME}"
17
+ VIAddVersionKey LegalCopyright "${COMPANY_NAME}"
18
+ VIAddVersionKey FileDescription "${APP_NAME}"
19
+ VIAddVersionKey FileVersion 1
20
+ VIAddVersionKey ProductVersion 1
21
+ VIAddVersionKey InternalName "${APP_NAME}"
22
+ VIAddVersionKey LegalTrademarks "${COMPANY_NAME}"
23
+ VIAddVersionKey OriginalFilename "${INSTALLER_FILE_NAME}"
24
+
25
+ InstallDir "$PROGRAMFILES64\${APP_NAME}"
26
+
27
+ !include "nsDialogs.nsh"
28
+
29
+ RequestExecutionLevel admin
30
+
31
+ Var WelcomeDialog
32
+ Var WelcomeLabel
33
+ Var alreadyInstalled
34
+ Var baseUrl
35
+ Var clientId
36
+ Var clientSecret
37
+ Var agentId
38
+
39
+ # The onInit handler is called when the installer is nearly finished initializing.
40
+ # See: https://nsis.sourceforge.io/Reference/.onInit
41
+ Function .onInit
42
+ ${If} ${FileExists} "$INSTDIR\winsw.xml"
43
+ StrCpy $alreadyInstalled 1
44
+ ${EndIf}
45
+ FunctionEnd
46
+
47
+ Page custom WelcomePage
48
+ Page custom InputPage InputPageLeave
49
+ Page instfiles
50
+
51
+ # The WelcomePage is a simple static screen that displays a friendly message.
52
+ Function WelcomePage
53
+ nsDialogs::Create 1018
54
+ Pop $WelcomeDialog
55
+
56
+ ${If} $WelcomeDialog == error
57
+ Abort
58
+ ${EndIf}
59
+
60
+ ${NSD_CreateLabel} 0 0 100% 50u "Welcome to the ${APP_NAME} Installer!$\r$\n$\r$\nClick next to continue."
61
+ Pop $WelcomeLabel
62
+
63
+ nsDialogs::Show
64
+ FunctionEnd
65
+
66
+ # The InputPage captures all of the user input for the agent.
67
+ Function InputPage
68
+ ${If} $alreadyInstalled == 1
69
+ Abort ; This skips the page
70
+ ${EndIf}
71
+
72
+ nsDialogs::Create 1018
73
+ Pop $0
74
+
75
+ StrCpy $baseUrl "${DEFAULT_BASE_URL}"
76
+ ${NSD_CreateLabel} 0 0 30% 12u "Base URL:"
77
+ Pop $R0
78
+ ${NSD_CreateText} 35% 0 65% 12u $baseUrl
79
+ Pop $R1
80
+
81
+ ${NSD_CreateLabel} 0 15u 30% 12u "Client ID:"
82
+ Pop $R2
83
+ ${NSD_CreateText} 35% 15u 65% 12u $clientId
84
+ Pop $R3
85
+
86
+ ${NSD_CreateLabel} 0 30u 30% 12u "Client Secret:"
87
+ Pop $R4
88
+ ${NSD_CreateText} 35% 30u 65% 12u $clientSecret
89
+ Pop $R5
90
+
91
+ ${NSD_CreateLabel} 0 45u 30% 12u "Agent ID:"
92
+ Pop $R6
93
+ ${NSD_CreateText} 35% 45u 65% 12u $agentId
94
+ Pop $R7
95
+
96
+ ${NSD_SetFocus} $R3
97
+ nsDialogs::Show
98
+ FunctionEnd
99
+
100
+ Function InputPageLeave
101
+ ${NSD_GetText} $R1 $baseUrl
102
+ ${NSD_GetText} $R3 $clientId
103
+ ${NSD_GetText} $R5 $clientSecret
104
+ ${NSD_GetText} $R7 $agentId
105
+ FunctionEnd
106
+
107
+ # Main installation entry point.
108
+ Section
109
+ DetailPrint "${APP_NAME}"
110
+ SetOutPath "$INSTDIR"
111
+
112
+ ${If} $alreadyInstalled == 1
113
+ Call UpgradeApp
114
+ ${Else}
115
+ Call InstallApp
116
+ ${EndIf}
117
+
118
+ SectionEnd
119
+
120
+ # Upgrade an existing installation.
121
+ # This only copies files, and restarts the Windows Service.
122
+ # It does not modify the existing configuration settings.
123
+ Function UpgradeApp
124
+
125
+ # Stop the service
126
+ DetailPrint "Stopping service..."
127
+ ExecWait "winsw.exe stop --force" $1
128
+ DetailPrint "Stop service returned $1"
129
+
130
+ # Sleep for 3 seconds to let the service fully stop
131
+ # We cannot write the new version of the exe while the process is running
132
+ DetailPrint "Sleeping..."
133
+ Sleep 3000
134
+
135
+ # Copy the new files to the installation directory
136
+ File dist\medplum-agent-win-x64.exe
137
+ File README.md
138
+
139
+ # Start the service
140
+ DetailPrint "Starting service..."
141
+ ExecWait "winsw.exe start" $1
142
+ DetailPrint "Start service returned $1"
143
+
144
+ FunctionEnd
145
+
146
+ # Do the actual installation.
147
+ # Install all of the files.
148
+ # Install the Windows Service.
149
+ Function InstallApp
150
+ # Print user input
151
+ DetailPrint "Base URL: $baseUrl"
152
+ DetailPrint "Client ID: $clientId"
153
+ DetailPrint "Client Secret: $clientSecret"
154
+ DetailPrint "Agent ID: $agentId"
155
+
156
+ # Copy the service files to the root directory
157
+ File ..\..\node_modules\node-windows\bin\winsw\winsw.exe
158
+ File dist\medplum-agent-win-x64.exe
159
+ File README.md
160
+
161
+ # Create the winsw.xml config file
162
+ # See config file format: https://github.com/winsw/winsw/blob/v3/docs/xml-config-file.md
163
+ FileOpen $9 winsw.xml w
164
+ FileWrite $9 "<service>$\r$\n"
165
+ FileWrite $9 "<id>${SERVICE_NAME}</id>$\r$\n"
166
+ FileWrite $9 "<name>${APP_NAME}</name>$\r$\n"
167
+ FileWrite $9 "<description>Securely connects local devices to ${COMPANY_NAME} cloud</description>$\r$\n"
168
+ FileWrite $9 "<executable>$INSTDIR\medplum-agent-win-x64.exe</executable>$\r$\n"
169
+ FileWrite $9 "<arguments>$\"$baseUrl$\" $\"$clientId$\" $\"$clientSecret$\" $\"$agentId$\"</arguments>$\r$\n"
170
+ FileWrite $9 "<startmode>Automatic</startmode>$\r$\n"
171
+ FileWrite $9 "</service>$\r$\n"
172
+ FileClose $9
173
+
174
+ # Install the service
175
+ DetailPrint "Installing service..."
176
+ ExecWait "winsw.exe install" $1
177
+ DetailPrint "Install returned $1"
178
+
179
+ # Start the service
180
+ DetailPrint "Starting service..."
181
+ ExecWait "winsw.exe start" $1
182
+ DetailPrint "Start service returned $1"
183
+
184
+ # Create the uninstaller
185
+ DetailPrint "Creating the uninstaller..."
186
+ SetOutPath $INSTDIR
187
+ WriteUninstaller "$INSTDIR\uninstall.exe"
188
+
189
+ # Register the uninstaller
190
+ DetailPrint "Registering the uninstaller..."
191
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "DisplayName" "${APP_NAME}"
192
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
193
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
194
+ DetailPrint "Uninstaller complete"
195
+
196
+ # Create Start menu shortcuts
197
+ DetailPrint "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk"
198
+ CreateDirectory "$SMPROGRAMS\${APP_NAME}"
199
+ CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk" "$INSTDIR\uninstall.exe"
200
+
201
+ FunctionEnd
202
+
203
+ # Start the uninstaller
204
+ Section Uninstall
205
+
206
+ # Uninstall the service
207
+ DetailPrint "Uninstalling service..."
208
+ SetOutPath "$INSTDIR"
209
+ ExecWait "winsw.exe uninstall" $1
210
+ DetailPrint "Uninstall returned $1"
211
+
212
+ # Get out of the service directory so we can delete it
213
+ SetOutPath "$PROGRAMFILES64"
214
+
215
+ # Uninstall the Start menu shortcuts
216
+ RMDir /r /REBOOTOK "$SMPROGRAMS\${APP_NAME}"
217
+
218
+ # Delete the files
219
+ RMDir /r /REBOOTOK "$INSTDIR"
220
+
221
+ # Unregister the program
222
+ DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}"
223
+
224
+ SectionEnd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/agent",
3
- "version": "2.0.30",
3
+ "version": "2.0.32",
4
4
  "description": "Medplum Agent",
5
5
  "author": "Medplum <hello@medplum.com>",
6
6
  "license": "Apache-2.0",
@@ -18,7 +18,8 @@
18
18
  "build": "npm run clean && tsc && node esbuild.mjs",
19
19
  "test": "jest",
20
20
  "agent": "ts-node src/main.ts",
21
- "package": "pkg ./dist/cjs/index.cjs --targets node18-win-x64 --output dist/medplum-agent-win-x64.exe"
21
+ "package": "pkg ./dist/cjs/index.cjs --targets node18-win-x64 --output dist/medplum-agent-win-x64.exe --options no-warnings",
22
+ "installer": "makensis installer.nsi"
22
23
  },
23
24
  "dependencies": {
24
25
  "@medplum/core": "*",
package/src/main.test.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { allOk, Hl7Message } from '@medplum/core';
2
- import { Bot, Resource } from '@medplum/fhirtypes';
1
+ import { allOk, createReference, Hl7Message } from '@medplum/core';
2
+ import { Agent, Bot, Endpoint, Resource } from '@medplum/fhirtypes';
3
3
  import { Hl7Client } from '@medplum/hl7';
4
4
  import { MockClient } from '@medplum/mock';
5
5
  import { Server } from 'mock-socket';
@@ -9,6 +9,7 @@ jest.mock('node-windows');
9
9
 
10
10
  const medplum = new MockClient();
11
11
  let bot: Bot;
12
+ let endpoint: Endpoint;
12
13
 
13
14
  describe('Agent', () => {
14
15
  beforeAll(async () => {
@@ -19,18 +20,26 @@ describe('Agent', () => {
19
20
  });
20
21
 
21
22
  bot = await medplum.createResource<Bot>({ resourceType: 'Bot' });
23
+
24
+ endpoint = await medplum.createResource<Endpoint>({
25
+ resourceType: 'Endpoint',
26
+ address: 'mllp://0.0.0.0:56000',
27
+ });
22
28
  });
23
29
 
24
30
  test('Runs successfully', async () => {
25
- const app = new App(medplum, { botId: bot.id as string });
26
- app.start();
27
- app.stop();
28
- app.stop();
29
- });
31
+ const agent = await medplum.createResource<Agent>({
32
+ resourceType: 'Agent',
33
+ channel: [
34
+ {
35
+ endpoint: createReference(endpoint),
36
+ targetReference: createReference(bot),
37
+ },
38
+ ],
39
+ });
30
40
 
31
- test('Use system event log', async () => {
32
- const app = new App(medplum, { botId: bot.id as string, useSystemEventLog: true });
33
- app.start();
41
+ const app = new App(medplum, agent.id as string);
42
+ await app.start();
34
43
  app.stop();
35
44
  app.stop();
36
45
  });
@@ -66,8 +75,18 @@ describe('Agent', () => {
66
75
  });
67
76
  });
68
77
 
69
- const app = new App(medplum, { botId: bot.id as string });
70
- app.start();
78
+ const agent = await medplum.createResource<Agent>({
79
+ resourceType: 'Agent',
80
+ channel: [
81
+ {
82
+ endpoint: createReference(endpoint),
83
+ targetReference: createReference(bot),
84
+ },
85
+ ],
86
+ });
87
+
88
+ const app = new App(medplum, agent.id as string);
89
+ await app.start();
71
90
 
72
91
  const client = new Hl7Client({
73
92
  host: 'localhost',
package/src/main.ts CHANGED
@@ -1,66 +1,97 @@
1
- import { Hl7Message, MedplumClient } from '@medplum/core';
1
+ import { Hl7Message, MedplumClient, resolveId } from '@medplum/core';
2
+ import { AgentChannel, Bot, Endpoint, Reference } from '@medplum/fhirtypes';
2
3
  import { Hl7Connection, Hl7MessageEvent, Hl7Server } from '@medplum/hl7';
3
- import { readFileSync } from 'fs';
4
4
  import { EventLogger } from 'node-windows';
5
5
  import WebSocket from 'ws';
6
6
 
7
- export interface AgentConfig {
8
- botId: string;
9
- useSystemEventLog?: boolean;
10
- }
11
-
12
7
  export class App {
13
8
  readonly log: EventLogger;
14
- readonly server: Hl7Server;
15
- readonly connections: Connection[] = [];
16
-
17
- constructor(readonly medplum: MedplumClient, readonly config: AgentConfig) {
18
- if (config.useSystemEventLog) {
19
- this.log = new EventLogger({
20
- source: 'MedplumService',
21
- eventLog: 'SYSTEM',
22
- });
23
- } else {
24
- this.log = {
25
- info: console.log,
26
- warn: console.warn,
27
- error: console.error,
28
- } as EventLogger;
9
+ readonly channels: AgentHl7Channel[];
10
+
11
+ constructor(
12
+ readonly medplum: MedplumClient,
13
+ readonly agentId: string
14
+ ) {
15
+ this.log = {
16
+ info: console.log,
17
+ warn: console.warn,
18
+ error: console.error,
19
+ } as EventLogger;
20
+
21
+ this.channels = [];
22
+ }
23
+
24
+ async start(): Promise<void> {
25
+ this.log.info('Medplum service starting...');
26
+
27
+ const agent = await this.medplum.readResource('Agent', this.agentId);
28
+
29
+ for (const definition of agent.channel as AgentChannel[]) {
30
+ const endpoint = await this.medplum.readReference(definition.endpoint as Reference<Endpoint>);
31
+ const channel = new AgentHl7Channel(this, definition, endpoint);
32
+ channel.start();
33
+ this.channels.push(channel);
29
34
  }
30
35
 
36
+ this.log.info('Medplum service started successfully');
37
+ }
38
+
39
+ stop(): void {
40
+ this.log.info('Medplum service stopping...');
41
+ this.channels.forEach((channel) => channel.stop());
42
+ this.log.info('Medplum service stopped successfully');
43
+ }
44
+ }
45
+
46
+ export class AgentHl7Channel {
47
+ readonly server: Hl7Server;
48
+ readonly connections: AgentHl7ChannelConnection[] = [];
49
+
50
+ constructor(
51
+ readonly app: App,
52
+ readonly definition: AgentChannel,
53
+ readonly endpoint: Endpoint
54
+ ) {
31
55
  this.server = new Hl7Server((connection) => {
32
- this.log.info('HL7 connection established');
33
- this.connections.push(new Connection(this, connection));
56
+ this.app.log.info('HL7 connection established');
57
+ this.connections.push(new AgentHl7ChannelConnection(this, connection));
34
58
  });
35
59
  }
36
60
 
37
61
  start(): void {
38
- this.log.info('Medplum service starting...');
39
- this.server.start(56000);
40
- this.log.info('Medplum service started successfully');
62
+ const address = new URL(this.endpoint.address as string);
63
+ this.app.log.info(`Channel starting on ${address}`);
64
+ this.server.start(parseInt(address.port, 10));
65
+ this.app.log.info('Channel started successfully');
41
66
  }
42
67
 
43
68
  stop(): void {
44
- this.log.info('Medplum service stopping...');
69
+ this.app.log.info('Channel stopping...');
45
70
  for (const connection of this.connections) {
46
71
  connection.close();
47
72
  }
48
73
  this.server.stop();
49
- this.log.info('Medplum service stopped successfully');
74
+ this.app.log.info('Channel stopped successfully');
50
75
  }
51
76
  }
52
77
 
53
- export class Connection {
78
+ export class AgentHl7ChannelConnection {
54
79
  readonly webSocket: WebSocket;
55
80
  readonly webSocketQueue: Hl7Message[] = [];
56
81
  readonly hl7ConnectionQueue: Hl7Message[] = [];
57
82
  live = false;
58
83
 
59
- constructor(readonly app: App, readonly hl7Connection: Hl7Connection) {
84
+ constructor(
85
+ readonly channel: AgentHl7Channel,
86
+ readonly hl7Connection: Hl7Connection
87
+ ) {
88
+ const app = channel.app;
89
+ const medplum = app.medplum;
90
+
60
91
  // Add listener immediately to handle incoming messages
61
92
  this.hl7Connection.addEventListener('message', (event) => this.handler(event));
62
93
 
63
- const webSocketUrl = new URL(this.app.medplum.getBaseUrl());
94
+ const webSocketUrl = new URL(medplum.getBaseUrl());
64
95
  webSocketUrl.protocol = webSocketUrl.protocol === 'https:' ? 'wss:' : 'ws:';
65
96
  webSocketUrl.pathname = '/ws/agent';
66
97
  console.log('Connecting to WebSocket:', webSocketUrl.href);
@@ -72,8 +103,9 @@ export class Connection {
72
103
  this.webSocket.send(
73
104
  JSON.stringify({
74
105
  type: 'connect',
75
- accessToken: this.app.medplum.getAccessToken(),
76
- botId: this.app.config.botId,
106
+ accessToken: medplum.getAccessToken(),
107
+ agentId: channel.app.agentId,
108
+ botId: resolveId(channel.definition.targetReference as Reference<Bot>),
77
109
  })
78
110
  );
79
111
  });
@@ -119,6 +151,7 @@ export class Connection {
119
151
  this.webSocket.send(
120
152
  JSON.stringify({
121
153
  type: 'transmit',
154
+ forwardedFor: this.hl7Connection.socket.remoteAddress,
122
155
  message: msg.toString(),
123
156
  })
124
157
  );
@@ -143,6 +176,15 @@ export class Connection {
143
176
  }
144
177
 
145
178
  if (typeof require !== 'undefined' && require.main === module) {
146
- const config = JSON.parse(readFileSync('medplum.config.json', 'utf8'));
147
- new App(new MedplumClient(config), config).start();
179
+ if (process.argv.length < 6) {
180
+ console.log('Usage: node medplum-agent.js <baseUrl> <clientId> <clientSecret> <agentId>');
181
+ process.exit(1);
182
+ }
183
+
184
+ const [_node, _script, baseUrl, clientId, clientSecret, agentId] = process.argv;
185
+ const medplum = new MedplumClient({ baseUrl, clientId });
186
+ medplum
187
+ .startClientLogin(clientId, clientSecret)
188
+ .then(() => new App(medplum, agentId).start())
189
+ .catch(console.error);
148
190
  }