@medplum/agent 4.3.15 → 4.4.1

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 (2) hide show
  1. package/dist/cjs/index.cjs +231 -30
  2. package/package.json +5 -5
@@ -7921,7 +7921,7 @@ var require_websocket = __commonJS({
7921
7921
  var EventEmitter = require("events");
7922
7922
  var https = require("https");
7923
7923
  var http = require("http");
7924
- var net = require("net");
7924
+ var net2 = require("net");
7925
7925
  var tls = require("tls");
7926
7926
  var { randomBytes, createHash } = require("crypto");
7927
7927
  var { Duplex, Readable: Readable2 } = require("stream");
@@ -8652,12 +8652,12 @@ var require_websocket = __commonJS({
8652
8652
  }
8653
8653
  function netConnect(options) {
8654
8654
  options.path = options.socketPath;
8655
- return net.connect(options);
8655
+ return net2.connect(options);
8656
8656
  }
8657
8657
  function tlsConnect(options) {
8658
8658
  options.path = void 0;
8659
8659
  if (!options.servername && options.servername !== "") {
8660
- options.servername = net.isIP(options.host) ? "" : options.host;
8660
+ options.servername = net2.isIP(options.host) ? "" : options.host;
8661
8661
  }
8662
8662
  return tls.connect(options);
8663
8663
  }
@@ -68386,7 +68386,7 @@ function Re() {
68386
68386
  return (r6 === "x" ? e : e & 3 | 8).toString(16);
68387
68387
  });
68388
68388
  }
68389
- var A = { CSS: "text/css", DICOM: "application/dicom", FAVICON: "image/vnd.microsoft.icon", FHIR_JSON: "application/fhir+json", FORM_URL_ENCODED: "application/x-www-form-urlencoded", HL7_V2: "x-application/hl7-v2+er7", HTML: "text/html", JAVASCRIPT: "text/javascript", JSON: "application/json", JSON_PATCH: "application/json-patch+json", JWT: "application/jwt", MULTIPART_FORM_DATA: "multipart/form-data", PNG: "image/png", SCIM_JSON: "application/scim+json", SVG: "image/svg+xml", TEXT: "text/plain", TYPESCRIPT: "text/typescript", PING: "x-application/ping", XML: "text/xml", CDA_XML: "application/cda+xml" };
68389
+ var A = { CSS: "text/css", DICOM: "application/dicom", FAVICON: "image/vnd.microsoft.icon", FHIR_JSON: "application/fhir+json", FORM_URL_ENCODED: "application/x-www-form-urlencoded", HL7_V2: "x-application/hl7-v2+er7", HTML: "text/html", JAVASCRIPT: "text/javascript", JSON: "application/json", JSON_PATCH: "application/json-patch+json", JWT: "application/jwt", MULTIPART_FORM_DATA: "multipart/form-data", PNG: "image/png", SCIM_JSON: "application/scim+json", SVG: "image/svg+xml", TEXT: "text/plain", TYPESCRIPT: "text/typescript", PING: "x-application/ping", XML: "text/xml", CDA_XML: "application/cda+xml", OCTET_STREAM: "application/octet-stream" };
68390
68390
  var Gr = class {
68391
68391
  constructor() {
68392
68392
  this.listeners = {};
@@ -69008,7 +69008,7 @@ var $t = class {
69008
69008
  return this.masterSubEmitter || (this.masterSubEmitter = new et(...Array.from(this.criteriaEntries.keys()))), this.masterSubEmitter;
69009
69009
  }
69010
69010
  };
69011
- var Yr = "4.3.15-dbf6882a3";
69011
+ var Yr = "4.4.1-672cfe139";
69012
69012
  var va = A.FHIR_JSON + ", */*; q=0.1";
69013
69013
  var Ta = "https://api.medplum.com/";
69014
69014
  var Sa = 1e3;
@@ -70323,7 +70323,7 @@ var H = class {
70323
70323
  // src/app.ts
70324
70324
  var import_node_child_process = require("node:child_process");
70325
70325
  var import_node_fs4 = require("node:fs");
70326
- var import_node_net3 = require("node:net");
70326
+ var import_node_net4 = require("node:net");
70327
70327
  var import_node_os4 = require("node:os");
70328
70328
  var import_node_process2 = __toESM(require("node:process"));
70329
70329
  var semver = __toESM(require_semver2());
@@ -70336,6 +70336,11 @@ var import_websocket = __toESM(require_websocket(), 1);
70336
70336
  var import_websocket_server = __toESM(require_websocket_server(), 1);
70337
70337
  var wrapper_default = import_websocket.default;
70338
70338
 
70339
+ // src/bytestream.ts
70340
+ var import_node_assert2 = __toESM(require("node:assert"));
70341
+ var import_node_crypto = require("node:crypto");
70342
+ var import_node_net3 = __toESM(require("node:net"));
70343
+
70339
70344
  // src/channel.ts
70340
70345
  var BaseChannel = class {
70341
70346
  constructor(app, definition, endpoint) {
@@ -70352,7 +70357,8 @@ var BaseChannel = class {
70352
70357
  };
70353
70358
  var ChannelType = {
70354
70359
  HL7_V2: "HL7_V2",
70355
- DICOM: "DICOM"
70360
+ DICOM: "DICOM",
70361
+ BYTE_STREAM: "BYTE_STREAM"
70356
70362
  };
70357
70363
  function getChannelType(endpoint) {
70358
70364
  if (endpoint.address.startsWith("dicom")) {
@@ -70361,19 +70367,182 @@ function getChannelType(endpoint) {
70361
70367
  if (endpoint.address.startsWith("mllp")) {
70362
70368
  return ChannelType.HL7_V2;
70363
70369
  }
70370
+ if (endpoint.address.startsWith("tcp")) {
70371
+ return ChannelType.BYTE_STREAM;
70372
+ }
70364
70373
  throw new Error(`Unsupported endpoint type: ${endpoint.address}`);
70365
70374
  }
70366
70375
  function getChannelTypeShortName(endpoint) {
70367
- switch (getChannelType(endpoint)) {
70368
- case ChannelType.HL7_V2:
70369
- return "HL7";
70370
- case ChannelType.DICOM:
70371
- return "DICOM";
70372
- default:
70373
- throw new Error(`Invalid endpoint type with address '${endpoint.address}'`);
70376
+ try {
70377
+ const channelType = getChannelType(endpoint);
70378
+ switch (channelType) {
70379
+ case ChannelType.HL7_V2:
70380
+ return "HL7";
70381
+ case ChannelType.DICOM:
70382
+ return "DICOM";
70383
+ case ChannelType.BYTE_STREAM:
70384
+ return "Byte Stream";
70385
+ default:
70386
+ channelType;
70387
+ throw new Error("Unreachable");
70388
+ }
70389
+ } catch (err) {
70390
+ throw new Error(`Invalid endpoint type with address '${endpoint.address}'`, { cause: err });
70374
70391
  }
70375
70392
  }
70376
70393
 
70394
+ // src/bytestream.ts
70395
+ var AgentByteStreamChannel = class extends BaseChannel {
70396
+ constructor(app, definition, endpoint) {
70397
+ super(app, definition, endpoint);
70398
+ this.started = false;
70399
+ this.connections = /* @__PURE__ */ new Map();
70400
+ this.startChar = -1;
70401
+ this.endChar = -1;
70402
+ this.app = app;
70403
+ this.server = import_node_net3.default.createServer((socket) => this.handleNewConnection(socket));
70404
+ this.log = app.log.clone({ options: { prefix: `[Byte Stream:${definition.name}] ` } });
70405
+ this.channelLog = app.channelLog.clone({ options: { prefix: `[Byte Stream:${definition.name}] ` } });
70406
+ }
70407
+ start() {
70408
+ if (this.started) {
70409
+ return;
70410
+ }
70411
+ this.started = true;
70412
+ const address = new URL(this.getEndpoint().address);
70413
+ this.log.info(`Channel starting on ${address}...`);
70414
+ this.configureTcpServerAndConnections();
70415
+ this.server.listen(Number.parseInt(address.port, 10));
70416
+ this.log.info("Channel started successfully");
70417
+ }
70418
+ async stop() {
70419
+ if (!this.started) {
70420
+ return;
70421
+ }
70422
+ this.log.info("Channel stopping...");
70423
+ for (const [_, connection] of this.connections) {
70424
+ connection.close();
70425
+ }
70426
+ await new Promise((resolve2, reject) => {
70427
+ this.server.close((err) => {
70428
+ if (err) {
70429
+ reject(err);
70430
+ } else {
70431
+ resolve2();
70432
+ }
70433
+ });
70434
+ });
70435
+ this.started = false;
70436
+ this.log.info("Channel stopped successfully");
70437
+ }
70438
+ async reloadConfig(definition, endpoint) {
70439
+ const previousEndpoint = this.endpoint;
70440
+ this.definition = definition;
70441
+ this.endpoint = endpoint;
70442
+ this.log.info("Reloading config... Evaluating if channel needs to change address...");
70443
+ if (this.needToRebindToPort(previousEndpoint, endpoint)) {
70444
+ await this.stop();
70445
+ this.start();
70446
+ this.log.info(`Address changed: ${previousEndpoint.address} => ${endpoint.address}`);
70447
+ } else if (previousEndpoint.address !== endpoint.address) {
70448
+ this.log.info(
70449
+ `Reconfiguring TCP server and ${this.connections.size} connections based on new endpoint settings: ${previousEndpoint.address} => ${endpoint.address}`
70450
+ );
70451
+ this.configureTcpServerAndConnections();
70452
+ } else {
70453
+ this.log.info(`No address change needed. Listening at ${endpoint.address}`);
70454
+ }
70455
+ }
70456
+ needToRebindToPort(firstEndpoint, secondEndpoint) {
70457
+ if (firstEndpoint.address === secondEndpoint.address || new URL(firstEndpoint.address).port === new URL(secondEndpoint.address).port) {
70458
+ return false;
70459
+ }
70460
+ return true;
70461
+ }
70462
+ configureTcpServerAndConnections() {
70463
+ const address = new URL(this.getEndpoint().address);
70464
+ const startCharStr = address.searchParams.get("startChar");
70465
+ const endCharStr = address.searchParams.get("endChar");
70466
+ if (!(startCharStr && endCharStr)) {
70467
+ throw new Error(`Failed to parse startChar and/or endChar query param(s) from ${address}`);
70468
+ }
70469
+ this.startChar = startCharStr.codePointAt(0) ?? -1;
70470
+ this.endChar = endCharStr.codePointAt(0) ?? -1;
70471
+ (0, import_node_assert2.default)(this.startChar !== -1 && this.endChar !== -1);
70472
+ }
70473
+ sendToRemote(msg) {
70474
+ const connection = this.connections.get(msg.remote);
70475
+ if (connection) {
70476
+ connection.write(Buffer.from(msg.body, "hex"));
70477
+ }
70478
+ }
70479
+ handleNewConnection(socket) {
70480
+ const c2 = new ByteStreamChannelConnection(this, socket);
70481
+ this.log.info(`Byte stream connection established: ${c2.remote}`);
70482
+ this.connections.set(c2.remote, c2);
70483
+ }
70484
+ };
70485
+ var ByteStreamChannelConnection = class {
70486
+ constructor(channel, socket) {
70487
+ this.msgChunks = [];
70488
+ this.msgTotalLength = -1;
70489
+ this.channel = channel;
70490
+ this.socket = socket;
70491
+ this.remote = `${socket.remoteAddress}:${socket.remotePort}`;
70492
+ this.socket.on("data", (data2) => this.handler(data2));
70493
+ }
70494
+ async handler(data2) {
70495
+ try {
70496
+ this.channel.channelLog.info(`Received: ${data2.toString("hex").replaceAll("\r", "\n")}`);
70497
+ let lastEndIndex = -1;
70498
+ for (let i = 0; i < data2.length; i++) {
70499
+ const char = data2[i];
70500
+ if (char === this.channel.startChar) {
70501
+ this.msgChunks.length = 0;
70502
+ this.msgTotalLength = 0;
70503
+ } else if (char === this.channel.endChar) {
70504
+ if (this.msgTotalLength === -1) {
70505
+ continue;
70506
+ }
70507
+ const startSlice = lastEndIndex + 1;
70508
+ const slice = data2.subarray(startSlice, i + 1);
70509
+ this.msgChunks.push(slice);
70510
+ this.msgTotalLength += slice.length;
70511
+ const messageBuffer = Buffer.concat(this.msgChunks, this.msgTotalLength);
70512
+ this.channel.app.addToWebSocketQueue({
70513
+ type: "agent:transmit:request",
70514
+ accessToken: "placeholder",
70515
+ channel: this.channel.getDefinition().name,
70516
+ remote: this.remote,
70517
+ contentType: A.OCTET_STREAM,
70518
+ body: messageBuffer.toString("hex"),
70519
+ callback: `Agent/${this.channel.app.agentId}-${(0, import_node_crypto.randomUUID)()}`
70520
+ });
70521
+ this.msgChunks.length = 0;
70522
+ lastEndIndex = i;
70523
+ this.msgTotalLength = -1;
70524
+ }
70525
+ }
70526
+ if (lastEndIndex < data2.length - 1) {
70527
+ const remainingSlice = data2.subarray(lastEndIndex + 1);
70528
+ if (remainingSlice.length > 0) {
70529
+ this.msgChunks.push(remainingSlice);
70530
+ this.msgTotalLength += remainingSlice.length;
70531
+ }
70532
+ }
70533
+ } catch (err) {
70534
+ this.channel.log.error(`Byte stream error occurred - check channel logs`);
70535
+ this.channel.channelLog.error(`Byte stream error: ${Ie(err)}`);
70536
+ }
70537
+ }
70538
+ write(data2) {
70539
+ this.socket.write(data2);
70540
+ }
70541
+ close() {
70542
+ this.socket.end();
70543
+ }
70544
+ };
70545
+
70377
70546
  // src/constants.ts
70378
70547
  var RETRY_WAIT_DURATION_MS = 1e4;
70379
70548
  var DEFAULT_PING_TIMEOUT = 3600;
@@ -70384,7 +70553,7 @@ var MAX_MISSED_HEARTBEATS = 1;
70384
70553
  // src/dicom.ts
70385
70554
  var dcmjs = __toESM(require_dcmjs());
70386
70555
  var dimse = __toESM(require_dcmjs_dimse_min());
70387
- var import_node_crypto = require("node:crypto");
70556
+ var import_node_crypto2 = require("node:crypto");
70388
70557
  var import_node_fs = require("node:fs");
70389
70558
  var import_node_os = require("node:os");
70390
70559
  var import_node_path = require("node:path");
@@ -70438,7 +70607,7 @@ var AgentDicomChannel = class extends BaseChannel {
70438
70607
  let binary = void 0;
70439
70608
  let dicomJson = void 0;
70440
70609
  if (dataset) {
70441
- const tempFileName = (0, import_node_path.join)(DcmjsDimseScp.channel.tempDir, (0, import_node_crypto.randomUUID)() + ".dcm");
70610
+ const tempFileName = (0, import_node_path.join)(DcmjsDimseScp.channel.tempDir, (0, import_node_crypto2.randomUUID)() + ".dcm");
70442
70611
  dataset.toFile(tempFileName);
70443
70612
  const buffer2 = (0, import_node_fs.readFileSync)(tempFileName);
70444
70613
  const medplum = App.instance.medplum;
@@ -70471,7 +70640,7 @@ var AgentDicomChannel = class extends BaseChannel {
70471
70640
  remote: this.association?.getCallingAeTitle(),
70472
70641
  contentType: A.JSON,
70473
70642
  body: JSON.stringify(payload),
70474
- callback: `Agent/${App.instance.agentId}-${(0, import_node_crypto.randomUUID)()}`
70643
+ callback: `Agent/${App.instance.agentId}-${(0, import_node_crypto2.randomUUID)()}`
70475
70644
  });
70476
70645
  response2.setStatus(dimse.constants.Status.Success);
70477
70646
  } catch (err) {
@@ -70548,7 +70717,7 @@ var AgentDicomChannel = class extends BaseChannel {
70548
70717
  };
70549
70718
 
70550
70719
  // src/hl7.ts
70551
- var import_node_crypto2 = require("node:crypto");
70720
+ var import_node_crypto3 = require("node:crypto");
70552
70721
 
70553
70722
  // src/stats.ts
70554
70723
  var currentStats = {
@@ -70677,7 +70846,7 @@ var AgentHl7ChannelConnection = class {
70677
70846
  remote: this.remote,
70678
70847
  contentType: A.HL7_V2,
70679
70848
  body: event.message.toString(),
70680
- callback: `Agent/${this.channel.app.agentId}-${(0, import_node_crypto2.randomUUID)()}`
70849
+ callback: `Agent/${this.channel.app.agentId}-${(0, import_node_crypto3.randomUUID)()}`
70681
70850
  });
70682
70851
  } catch (err) {
70683
70852
  this.channel.log.error(`HL7 error occurred - check channel logs`);
@@ -71449,6 +71618,7 @@ var App = class _App {
71449
71618
  pendingRemoval.delete(leftover);
71450
71619
  this.channels.delete(leftover);
71451
71620
  }
71621
+ const errors = [];
71452
71622
  for (let i = 0; i < filteredChannels.length; i++) {
71453
71623
  const definition = filteredChannels[i];
71454
71624
  const endpoint = filteredEndpoints[i];
@@ -71458,9 +71628,31 @@ var App = class _App {
71458
71628
  try {
71459
71629
  await this.startOrReloadChannel(definition, endpoint);
71460
71630
  } catch (err) {
71631
+ errors.push(err);
71461
71632
  this.log.error(Ie(err));
71462
71633
  }
71463
71634
  }
71635
+ if (errors.length) {
71636
+ throw new p({
71637
+ resourceType: "OperationOutcome",
71638
+ issue: [
71639
+ {
71640
+ severity: "error",
71641
+ code: "invalid",
71642
+ details: {
71643
+ text: `${errors.length} error(s) occurred while reloading channels`
71644
+ }
71645
+ },
71646
+ ...errors.map(
71647
+ (err) => ({
71648
+ severity: "error",
71649
+ code: "invalid",
71650
+ details: { text: Ie(err) }
71651
+ })
71652
+ )
71653
+ ]
71654
+ });
71655
+ }
71464
71656
  }
71465
71657
  /**
71466
71658
  * Validates whether all endpoints are valid. Also ensures that there are no conflicting ports between any endpoints in the group.
@@ -71503,15 +71695,24 @@ var App = class _App {
71503
71695
  await channel.reloadConfig(definition, endpoint);
71504
71696
  return;
71505
71697
  }
71506
- switch (getChannelType(endpoint)) {
71507
- case ChannelType.DICOM:
71508
- channel = new AgentDicomChannel(this, definition, endpoint);
71509
- break;
71510
- case ChannelType.HL7_V2:
71511
- channel = new AgentHl7Channel(this, definition, endpoint);
71512
- break;
71513
- default:
71514
- throw new Error(`Unsupported endpoint type: ${endpoint.address}`);
71698
+ try {
71699
+ const channelType = getChannelType(endpoint);
71700
+ switch (channelType) {
71701
+ case ChannelType.DICOM:
71702
+ channel = new AgentDicomChannel(this, definition, endpoint);
71703
+ break;
71704
+ case ChannelType.HL7_V2:
71705
+ channel = new AgentHl7Channel(this, definition, endpoint);
71706
+ break;
71707
+ case ChannelType.BYTE_STREAM:
71708
+ channel = new AgentByteStreamChannel(this, definition, endpoint);
71709
+ break;
71710
+ default:
71711
+ throw new Error(`Unsupported endpoint type: ${endpoint.address}`);
71712
+ }
71713
+ } catch (err) {
71714
+ this.log.error(Ie(err));
71715
+ return;
71515
71716
  }
71516
71717
  channel.start();
71517
71718
  this.channels.set(definition.name, channel);
@@ -71599,14 +71800,14 @@ var App = class _App {
71599
71800
  const warnMsg = "Message body present but unused. Body for a ping request should be empty or a message formatted as `PING[ count]`.";
71600
71801
  this.log.warn(warnMsg);
71601
71802
  }
71602
- if ((0, import_node_net3.isIPv6)(message.remote)) {
71803
+ if ((0, import_node_net4.isIPv6)(message.remote)) {
71603
71804
  const errMsg = `Attempted to ping an IPv6 address: ${message.remote}
71604
71805
 
71605
71806
  IPv6 is currently unsupported.`;
71606
71807
  this.log.error(errMsg);
71607
71808
  throw new Error(errMsg);
71608
71809
  }
71609
- if (!((0, import_node_net3.isIPv4)(message.remote) || _l(message.remote))) {
71810
+ if (!((0, import_node_net4.isIPv4)(message.remote) || _l(message.remote))) {
71610
71811
  const errMsg = `Attempted to ping an invalid host.
71611
71812
 
71612
71813
  "${message.remote}" is not a valid IPv4 address or a resolvable hostname.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/agent",
3
- "version": "4.3.15",
3
+ "version": "4.4.1",
4
4
  "description": "Medplum Agent",
5
5
  "homepage": "https://www.medplum.com/",
6
6
  "bugs": {
@@ -25,8 +25,8 @@
25
25
  "test": "jest"
26
26
  },
27
27
  "dependencies": {
28
- "@medplum/core": "4.3.15",
29
- "@medplum/hl7": "4.3.15",
28
+ "@medplum/core": "4.4.1",
29
+ "@medplum/hl7": "4.4.1",
30
30
  "dcmjs-dimse": "0.3.0",
31
31
  "iconv-lite": "0.7.0",
32
32
  "semver": "7.7.2",
@@ -35,8 +35,8 @@
35
35
  "ws": "8.18.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@medplum/fhirtypes": "4.3.15",
39
- "@medplum/mock": "4.3.15",
38
+ "@medplum/fhirtypes": "4.4.1",
39
+ "@medplum/mock": "4.4.1",
40
40
  "@types/async-eventemitter": "0.2.4",
41
41
  "@types/ws": "8.18.1",
42
42
  "mock-socket": "9.3.1",