@medplum/agent 2.0.30 → 2.0.31

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,199 @@
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 baseUrl
34
+ Var clientId
35
+ Var clientSecret
36
+ Var agentId
37
+
38
+ Page custom WelcomePage
39
+ Page custom InputPage InputPageLeave
40
+ Page instfiles
41
+
42
+ # The WelcomePage is a simple static screen that displays a friendly message.
43
+ Function WelcomePage
44
+ nsDialogs::Create 1018
45
+ Pop $WelcomeDialog
46
+
47
+ ${If} $WelcomeDialog == error
48
+ Abort
49
+ ${EndIf}
50
+
51
+ ${NSD_CreateLabel} 0 0 100% 50u "Welcome to the ${APP_NAME} Installer!$\r$\n$\r$\nClick next to continue."
52
+ Pop $WelcomeLabel
53
+
54
+ nsDialogs::Show
55
+ FunctionEnd
56
+
57
+ # The InputPage captures all of the user input for the agent.
58
+ Function InputPage
59
+ nsDialogs::Create 1018
60
+ Pop $0
61
+
62
+ StrCpy $baseUrl "${DEFAULT_BASE_URL}"
63
+ ${NSD_CreateLabel} 0 0 30% 12u "Base URL:"
64
+ Pop $R0
65
+ ${NSD_CreateText} 35% 0 65% 12u $baseUrl
66
+ Pop $R1
67
+
68
+ ${NSD_CreateLabel} 0 15u 30% 12u "Client ID:"
69
+ Pop $R2
70
+ ${NSD_CreateText} 35% 15u 65% 12u $clientId
71
+ Pop $R3
72
+
73
+ ${NSD_CreateLabel} 0 30u 30% 12u "Client Secret:"
74
+ Pop $R4
75
+ ${NSD_CreateText} 35% 30u 65% 12u $clientSecret
76
+ Pop $R5
77
+
78
+ ${NSD_CreateLabel} 0 45u 30% 12u "Agent ID:"
79
+ Pop $R6
80
+ ${NSD_CreateText} 35% 45u 65% 12u $agentId
81
+ Pop $R7
82
+
83
+ ${NSD_SetFocus} $R3
84
+ nsDialogs::Show
85
+ FunctionEnd
86
+
87
+ Function InputPageLeave
88
+ ${NSD_GetText} $R1 $baseUrl
89
+ ${NSD_GetText} $R3 $clientId
90
+ ${NSD_GetText} $R5 $clientSecret
91
+ ${NSD_GetText} $R7 $agentId
92
+ FunctionEnd
93
+
94
+ # Do the actual installation.
95
+ # Install all of the files.
96
+ # Install the Windows Service.
97
+ Section
98
+ DetailPrint "${APP_NAME}"
99
+
100
+ # Call userInfo plugin to get user info. The plugin puts the result in the stack
101
+ userInfo::getAccountType
102
+
103
+ # Pop the result from the stack into $0
104
+ Pop $0
105
+
106
+ # Compare the result with the string "Admin" to see if the user is admin.
107
+ # If match, jump 3 lines down.
108
+ strCmp $0 "Admin" +3
109
+
110
+ # If there is not a match, print message and return
111
+ DetailPrint "User is not admin: $0"
112
+ return
113
+
114
+ # Otherwise, confirm and return
115
+ DetailPrint "User is admin"
116
+
117
+ # Print user input
118
+ DetailPrint "Base URL: $baseUrl"
119
+ DetailPrint "Client ID: $clientId"
120
+ DetailPrint "Client Secret: $clientSecret"
121
+ DetailPrint "Agent ID: $agentId"
122
+
123
+ # Copy the service files to the root directory
124
+ SetOutPath "$INSTDIR"
125
+ File ..\..\node_modules\node-windows\bin\winsw\winsw.exe
126
+ File dist\medplum-agent-win-x64.exe
127
+ File README.md
128
+
129
+ # Create the winsw.xml config file
130
+ # See config file format: https://github.com/winsw/winsw/blob/v3/docs/xml-config-file.md
131
+ FileOpen $9 winsw.xml w
132
+ FileWrite $9 "<service>$\r$\n"
133
+ FileWrite $9 "<id>${SERVICE_NAME}</id>$\r$\n"
134
+ FileWrite $9 "<name>${APP_NAME}</name>$\r$\n"
135
+ FileWrite $9 "<description>Securely connects local devices to ${COMPANY_NAME} cloud</description>$\r$\n"
136
+ FileWrite $9 "<executable>$INSTDIR\medplum-agent-win-x64.exe</executable>$\r$\n"
137
+ FileWrite $9 "<arguments>$\"$baseUrl$\" $\"$clientId$\" $\"$clientSecret$\" $\"$agentId$\"</arguments>$\r$\n"
138
+ FileWrite $9 "<startmode>Automatic</startmode>$\r$\n"
139
+ FileWrite $9 "</service>$\r$\n"
140
+ FileClose $9
141
+
142
+ # Install the service
143
+ DetailPrint "Installing service..."
144
+ StrCpy $0 "winsw.exe install"
145
+ #DetailPrint "$0"
146
+ ExecWait $0 $1
147
+ DetailPrint "Install returned $1"
148
+
149
+ # Start the service
150
+ DetailPrint "Starting service..."
151
+ StrCpy $0 "winsw.exe start"
152
+ #DetailPrint "$0"
153
+ ExecWait $0 $1
154
+ DetailPrint "Start service returned $1"
155
+
156
+ # Create the uninstaller
157
+ DetailPrint "Creating the uninstaller..."
158
+ SetOutPath $INSTDIR
159
+ WriteUninstaller "$INSTDIR\uninstall.exe"
160
+
161
+ # Register the uninstaller
162
+ DetailPrint "Registering the uninstaller..."
163
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "DisplayName" "${APP_NAME}"
164
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
165
+ WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
166
+ DetailPrint "Uninstaller complete"
167
+
168
+ # Create Start menu shortcuts
169
+ DetailPrint "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk"
170
+ CreateDirectory "$SMPROGRAMS\${APP_NAME}"
171
+ CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Uninstall.lnk" "$INSTDIR\uninstall.exe"
172
+
173
+ # default section end
174
+ SectionEnd
175
+
176
+ # Start the uninstaller
177
+ Section Uninstall
178
+
179
+ # Uninstall the service
180
+ DetailPrint "Uninstalling service..."
181
+ SetOutPath "$INSTDIR"
182
+ StrCpy $0 "winsw.exe uninstall"
183
+ #DetailPrint "$0"
184
+ ExecWait $0 $1
185
+ DetailPrint "Uninstall returned $1"
186
+
187
+ # Get out of the service directory so we can delete it
188
+ SetOutPath "$PROGRAMFILES64"
189
+
190
+ # Uninstall the Start menu shortcuts
191
+ RMDir /r /REBOOTOK "$SMPROGRAMS\${APP_NAME}"
192
+
193
+ # Delete the files
194
+ RMDir /r /REBOOTOK "$INSTDIR"
195
+
196
+ # Unregister the program
197
+ DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${SERVICE_NAME}"
198
+
199
+ 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.31",
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,8 @@ 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
+ botId: resolveId(channel.definition.targetReference as Reference<Bot>),
77
108
  })
78
109
  );
79
110
  });
@@ -143,6 +174,15 @@ export class Connection {
143
174
  }
144
175
 
145
176
  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();
177
+ if (process.argv.length < 6) {
178
+ console.log('Usage: node medplum-agent.js <baseUrl> <clientId> <clientSecret> <agentId>');
179
+ process.exit(1);
180
+ }
181
+
182
+ const [_node, _script, baseUrl, clientId, clientSecret, agentId] = process.argv;
183
+ const medplum = new MedplumClient({ baseUrl, clientId });
184
+ medplum
185
+ .startClientLogin(clientId, clientSecret)
186
+ .then(() => new App(medplum, agentId).start())
187
+ .catch(console.error);
148
188
  }