positron.js 1.0.6 → 1.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.
@@ -127,6 +127,22 @@ func printError(_ message: String) {
127
127
  let tag = isWarning ? "WARNING" : (isInfo ? "INFO" : "ERROR")
128
128
 
129
129
  print("\(red)[SWIFT \(tag)] \(msg)\(reset)")
130
+
131
+ if(tag == "INFO") {
132
+ return
133
+ }
134
+
135
+ if msg.contains("IPC response") || msg.contains("WebSocket error") || msg.contains("Reconnecting to") || msg.contains("IPC message") {
136
+ return
137
+ }
138
+
139
+ if let ipcClient = AppDelegate.shared?.ipcClient {
140
+ ipcClient.send(
141
+ IPCResponse(windowId: -1, event: "nativeError", data: ["message": message, "type": tag])
142
+ )
143
+ } else {
144
+ print("\(red)[SWIFT ERROR] No IPC client available to send error message over. \(reset)")
145
+ }
130
146
  }
131
147
 
132
148
  public protocol PositronExtension {
@@ -370,6 +386,10 @@ case "forceCloseWindow":
370
386
  printError("loadURL — invalid or missing URL")
371
387
  return
372
388
  }
389
+ if let scheme = url.scheme?.lowercased(), !["http", "https", "file"].contains(scheme) {
390
+ printError("loadURL — blocked unauthorized URL scheme: \(scheme)")
391
+ return
392
+ }
373
393
  (window.contentView as? WKWebView)?.load(URLRequest(url: url))
374
394
 
375
395
  case "hide":
@@ -585,19 +605,6 @@ UNUserNotificationCenter.current().requestAuthorization(
585
605
  IPCResponse(windowId: windowId, event: args.last ?? "getTitle-reply-\(windowId)", data: ["title": title])
586
606
  )
587
607
 
588
- case "executeAppleScript":
589
- guard let scriptSource = args.first else {
590
- printError("executeAppleScript — missing script argument")
591
- return
592
- }
593
- let script = NSAppleScript(source: scriptSource)
594
- var errorInfo: NSDictionary?
595
- script?.executeAndReturnError(&errorInfo)
596
- if let errorInfo {
597
- let errorMessage = errorInfo[NSAppleScript.errorMessage] as? String ?? "Unknown error"
598
- printError("executeAppleScript failed: \(errorMessage)")
599
- }
600
-
601
608
  case "isVisible":
602
609
  guard let window = windows[windowId] else { return }
603
610
  let isVisible = window.isVisible
@@ -819,7 +826,7 @@ case "addToContentBlocker":
819
826
  alert.addButton(withTitle: "Cancel")
820
827
 
821
828
  alert.beginSheetModal(for: window) { response in
822
- let confirmed = (response == .alertFirstButtonReturn)
829
+ let confirmed = (response == .alertSecondButtonReturn) ? false : true
823
830
  AppDelegate.shared?.ipcClient.send(
824
831
  IPCResponse(windowId: windowId, event: args.last ?? "confirm-reply-\(windowId)", data: ["confirmed": confirmed ? "true" : "false"])
825
832
  )
@@ -926,6 +933,14 @@ final class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
926
933
  IPCResponse(windowId: windowId, event: eventName, data: ["url": webView.url?.absoluteString ?? "", "title": webView.title ?? "", "canGoBack": (webView.canGoBack ? "true" : "false"), "canGoForward": (webView.canGoForward ? "true" : "false")])
927
934
  )
928
935
  }
936
+
937
+ func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
938
+ printError("Navigation failed: \(error.localizedDescription)")
939
+ }
940
+
941
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
942
+ printError("Provisional navigation failed: \(error.localizedDescription)")
943
+ }
929
944
  }
930
945
 
931
946
  // MARK: - WebView → Swift IPC Handler
@@ -1035,9 +1050,9 @@ final class IPCClient {
1035
1050
  private let maxReconnectAttempts = 10
1036
1051
  private let reconnectDelay: TimeInterval = 2.0
1037
1052
 
1038
- init(serverURL: URL = URL(string: "ws://localhost:9000")!) {
1053
+ init(serverURL: URL = URL(string: "ws://127.0.0.1:9000")!) {
1039
1054
  let POSITRON_IPC_PORT = port ?? 9000
1040
- self.serverURL = URL(string: "ws://localhost:\(POSITRON_IPC_PORT)")!
1055
+ self.serverURL = URL(string: "ws://127.0.0.1:\(POSITRON_IPC_PORT)")!
1041
1056
  self.authToken = AUTH_TOKEN
1042
1057
  }
1043
1058
 
@@ -1052,7 +1067,6 @@ final class IPCClient {
1052
1067
  webSocketTask = session.webSocketTask(with: request)
1053
1068
  webSocketTask?.resume()
1054
1069
  printError("INFO: Connecting to IPC server (attempt \(reconnectAttempts + 1))…")
1055
- reconnectAttempts = 0 // reset on successful connect
1056
1070
  receiveMessage()
1057
1071
  }
1058
1072
 
@@ -1064,7 +1078,12 @@ final class IPCClient {
1064
1078
  }
1065
1079
  webSocketTask?.send(.string(text)) { error in
1066
1080
  if let error {
1067
- printError("Failed to send IPC response: \(error.localizedDescription)")
1081
+ let errorMsg = error.localizedDescription
1082
+ printError("Failed to send IPC response: \(errorMsg)")
1083
+ if errorMsg.lowercased().contains("bad response from the server") {
1084
+ printError("Fatal connection error (Unauthorized or Port Hijacked). Exiting immediately.")
1085
+ exit(1)
1086
+ }
1068
1087
  }
1069
1088
  }
1070
1089
  }
@@ -1074,9 +1093,15 @@ final class IPCClient {
1074
1093
  guard let self else { return }
1075
1094
  switch result {
1076
1095
  case .failure(let error):
1077
- printError("WebSocket error: \(error.localizedDescription).")
1096
+ let errorMsg = error.localizedDescription
1097
+ printError("WebSocket error: \(errorMsg).")
1098
+ if errorMsg.contains("503") || errorMsg.contains("401") || errorMsg.contains("403") || errorMsg.lowercased().contains("bad response from the server") {
1099
+ printError("Fatal connection error (Unauthorized or Port Hijacked). Exiting immediately.")
1100
+ exit(1)
1101
+ }
1078
1102
  self.scheduleReconnect()
1079
1103
  case .success(let message):
1104
+ self.reconnectAttempts = 0
1080
1105
  switch message {
1081
1106
  case .string(let text):
1082
1107
  self.parseAndDispatch(text)
@@ -1108,7 +1133,7 @@ final class IPCClient {
1108
1133
  reconnectAttempts += 1
1109
1134
  guard reconnectAttempts < maxReconnectAttempts else {
1110
1135
  printError("Exceeded maximum reconnect attempts (\(maxReconnectAttempts)). Giving up.")
1111
- return
1136
+ exit(1)
1112
1137
  }
1113
1138
  printError("Reconnecting to \(serverURL) in \(reconnectDelay)s… (attempt \(reconnectAttempts)/\(maxReconnectAttempts))")
1114
1139
  DispatchQueue.global().asyncAfter(deadline: .now() + reconnectDelay) { [weak self] in
package/core/win/main.cs CHANGED
@@ -131,6 +131,20 @@ namespace PositronWindows
131
131
 
132
132
  string reset = "\u001b[0m";
133
133
  Console.WriteLine($"{red}[C# {tag}] {message}{reset}");
134
+
135
+ if (_ipcClient != null) {
136
+ if (tag != "INFO") {
137
+ _ipcClient.Send(new IPCResponse
138
+ {
139
+ windowId = -1,
140
+ @event = "nativeError",
141
+ data = new() { { "message", message }, { "type", tag } }
142
+ });
143
+ }
144
+ } else
145
+ {
146
+ Console.WriteLine($"{red}[C# ERROR] No IPC client available to send error message over. {reset}");
147
+ }
134
148
  }
135
149
 
136
150
  private static readonly string AuthToken =
@@ -222,7 +236,7 @@ namespace PositronWindows
222
236
  };
223
237
 
224
238
  await Task.Delay(500);
225
- _ipcClient = new IPCClient(new Uri($"ws://localhost:{_ipcPort}"));
239
+ _ipcClient = new IPCClient(new Uri($"ws://127.0.0.1:{_ipcPort}"));
226
240
  _ = _ipcClient.ConnectAsync(AuthToken);
227
241
  }
228
242
 
@@ -377,12 +391,16 @@ private void StartNodeProcess(string workingDirectory, string backendExeName)
377
391
  {
378
392
  bool isFile = webView.Source != null && webView.Source.IsFile;
379
393
  string eventName = isFile ? $"loadFile-reply-{windowId}" : $"loadURL-reply-{windowId}";
380
- _ipcClient.Send(new IPCResponse
381
- {
382
- windowId = windowId,
383
- @event = eventName,
384
- data = new() { { "url", webView.Source?.ToString() ?? "" }, { "title", webView.CoreWebView2.DocumentTitle }, { "canGoBack", webView.CoreWebView2.CanGoBack.ToString().ToLower() }, { "canGoForward", webView.CoreWebView2.CanGoForward.ToString().ToLower() } }
385
- });
394
+ if (e.IsSuccess) {
395
+ _ipcClient.Send(new IPCResponse
396
+ {
397
+ windowId = windowId,
398
+ @event = eventName,
399
+ data = new() { { "url", webView.Source?.ToString() ?? "" }, { "title", webView.CoreWebView2.DocumentTitle }, { "canGoBack", webView.CoreWebView2.CanGoBack.ToString().ToLower() }, { "canGoForward", webView.CoreWebView2.CanGoForward.ToString().ToLower() } }
400
+ });
401
+ } else {
402
+ error($"Navigation failed: {e.WebErrorStatus}");
403
+ }
386
404
  };
387
405
 
388
406
  webView.CoreWebView2.ContextMenuRequested += (s, e) =>
@@ -624,14 +642,19 @@ case "forceCloseWindow":
624
642
 
625
643
  case "loadURL":
626
644
  if (!WindowsMap.TryGetValue(windowId, out _)) break;
627
- if (args.Count == 0)
645
+ if (args.Count == 0 || !Uri.TryCreate(args[0], UriKind.Absolute, out var loadUrlUri))
628
646
  {
629
647
  error("loadURL — invalid or missing URL");
630
648
  break;
631
649
  }
650
+ if (loadUrlUri.Scheme != Uri.UriSchemeHttp && loadUrlUri.Scheme != Uri.UriSchemeHttps && loadUrlUri.Scheme != Uri.UriSchemeFile)
651
+ {
652
+ error($"loadURL — blocked unauthorized URL scheme: {loadUrlUri.Scheme}");
653
+ break;
654
+ }
632
655
  {
633
656
  var wv = GetWebView(windowId);
634
- if (wv != null) wv.Source = new Uri(args[0]);
657
+ if (wv != null) wv.Source = loadUrlUri;
635
658
  }
636
659
  break;
637
660
 
@@ -1269,11 +1292,12 @@ case "setBounds":
1269
1292
  if (_reconnectAttempts >= MaxReconnectAttempts)
1270
1293
  {
1271
1294
  error($"Exceeded maximum reconnect attempts ({MaxReconnectAttempts}). Giving up.");
1272
- return;
1295
+ Environment.Exit(1);
1273
1296
  }
1274
1297
 
1275
1298
  try
1276
1299
  {
1300
+ if (_ws != null) _ws.Dispose();
1277
1301
  _ws = new ClientWebSocket();
1278
1302
  _ws.Options.SetRequestHeader("x-positron-auth-token", authToken);
1279
1303
  error($"INFO: Connecting to IPC server (attempt {_reconnectAttempts + 1})…");
@@ -1285,8 +1309,18 @@ case "setBounds":
1285
1309
  catch (Exception ex)
1286
1310
  {
1287
1311
  _reconnectAttempts++;
1288
- error($"WebSocket error: {ex.Message}. Reconnecting in {ReconnectDelayMs / 1000}s… (attempt {_reconnectAttempts}/{MaxReconnectAttempts})");
1289
- await Task.Delay(ReconnectDelayMs);
1312
+ string errorMsg = ex.Message;
1313
+ if (ex.InnerException != null) errorMsg += " | " + ex.InnerException.Message;
1314
+
1315
+ error($"WebSocket error: {errorMsg}. Reconnecting in {ReconnectDelayMs / 1000}s… (attempt {_reconnectAttempts}/{MaxReconnectAttempts})");
1316
+
1317
+ if (errorMsg.Contains("503") || errorMsg.Contains("401") || errorMsg.Contains("403"))
1318
+ {
1319
+ error("Fatal connection error (Unauthorized or Port Hijacked). Exiting immediately.");
1320
+ Environment.Exit(1);
1321
+ }
1322
+
1323
+ await Task.Delay(ReconnectDelayMs, _cts.Token);
1290
1324
  }
1291
1325
  }
1292
1326
  }
package/index.js CHANGED
@@ -37,12 +37,11 @@ const binaryPath = path.join(appRoot, "bin", binaryName);
37
37
 
38
38
  const appEvents = new Events.EventEmitter();
39
39
 
40
-
41
40
  const isPackaged = process.env.POSITRON_PACKAGED === "true";
42
41
 
43
42
  if(isPackaged) {
44
43
  if (typeof process.pkg !== 'undefined') {
45
- if (process.platform === 'darwin') {
44
+ if (process.platform === 'darwin' || process.platform === 'linux') {
46
45
  __dirname = path.join(path.dirname(process.execPath), '.');
47
46
  } else {
48
47
  __dirname = path.dirname(process.execPath);
@@ -52,11 +51,6 @@ if (typeof process.pkg !== 'undefined') {
52
51
 
53
52
  const EXPECTED_TOKEN = process.env.POSITRON_AUTH_TOKEN;
54
53
 
55
- const parseRes = (obj) => {
56
- if (Object.keys(obj) > 1) return obj;
57
-
58
- return Object.values(obj)[0];
59
- }
60
54
 
61
55
  if (!isPackaged) {
62
56
  // DEV MODE
@@ -69,6 +63,8 @@ if (!isPackaged) {
69
63
  }
70
64
  }
71
65
 
66
+ // setTimeout(() => { // FOR HIJACK TESTING
67
+
72
68
  info("Starting Positron render process...");
73
69
  const renderProcess = cp.spawn(binaryPath, {
74
70
  env: {
@@ -102,12 +98,20 @@ process.on("uncaughtException", (err) => {
102
98
  info(`[Positron] Render process exited with code ${code}`);
103
99
  process.exit(code);
104
100
  });
101
+ // }, 60000);
105
102
  } else {
106
103
  // PRODUCTION MODE
107
104
  info("[Positron] Packaged mode detected. Skipping native binary spawn.");
108
105
  }
109
106
 
110
107
  const httpServer = http.createServer((req, res) => {
108
+ const clientToken = req.headers["x-positron-auth-token"];
109
+ if (clientToken !== EXPECTED_TOKEN) {
110
+ res.writeHead(401, { 'Content-Type': 'text/plain' });
111
+ res.end('Unauthorized');
112
+ return;
113
+ }
114
+
111
115
  if (req.method === 'GET' && req.url === '/running') {
112
116
  res.writeHead(200, { 'Content-Type': 'text/plain' });
113
117
  res.end('true');
@@ -117,7 +121,25 @@ const httpServer = http.createServer((req, res) => {
117
121
  }
118
122
  });
119
123
 
120
- const _ipcWS = new WebSocket.Server({ server: httpServer });
124
+ const MAX_CONNECTIONS = 1;
125
+
126
+ const _ipcWS = new WebSocket.Server({ server: httpServer, verifyClient: (info, cb) => {
127
+
128
+ const clientToken = info.req.headers["x-positron-auth-token"];
129
+ if (clientToken !== EXPECTED_TOKEN) {
130
+ warn("[Security] Unauthorized local connection attempt rejected.");
131
+ cb(false, 401, "Unauthorized token match failure.");
132
+ return
133
+ }
134
+
135
+ if (_ipcWS.clients.size >= MAX_CONNECTIONS) {
136
+ return cb(false, 503, 'IPC client already connected. Only one client allowed at a time.');
137
+ }
138
+
139
+ cb(true);
140
+ }
141
+
142
+ });
121
143
  let activeSocket = null;
122
144
  const pendingWindows = new Set();
123
145
 
@@ -127,14 +149,6 @@ const commandQueue = [];
127
149
  let activeWindows = new Set();
128
150
 
129
151
  _ipcWS.on("connection", (ws, req) => {
130
- const clientToken = req.headers["x-positron-auth-token"];
131
-
132
- if (clientToken !== EXPECTED_TOKEN) {
133
- warn("[Security] Unauthorized local connection attempt rejected. Token:", clientToken, "Expected:", EXPECTED_TOKEN);
134
- ws.close(4001, "Unauthorized token match failure.");
135
- return;
136
- }
137
-
138
152
  activeSocket = ws;
139
153
  success("Client connected to IPC");
140
154
 
@@ -154,7 +168,7 @@ ws.on("message", raw => {
154
168
 
155
169
  if(process.env.POSITRON_LOG_IPC) console.log("Received IPC message:", msg);
156
170
 
157
- if (msg.event === "ipcMessage" || msg.event.includes("-reply-") || msg.event.includes("-result-")) {
171
+ if (msg.event === "ipcMessage" || msg.event.includes("-reply-") || msg.event.includes("-result-") || msg.event === "nativeError") {
158
172
 
159
173
  const simulatedMsg = msg.event === "ipcMessage" ? msg : {
160
174
  event: "ipcMessage",
@@ -216,7 +230,7 @@ ws.on("message", raw => {
216
230
  appEvents.emit(msg.event, msg.data);
217
231
  }
218
232
  } catch (err) {
219
- error("Failed to process incoming IPC network frame:", err);
233
+ error("Failed to process incoming IPC network frame:", err, err.stack.split('\n').slice(1).join('\n'));
220
234
  }
221
235
  });
222
236
 
@@ -243,12 +257,20 @@ class Window extends Events.EventEmitter {
243
257
  minimizable: true,
244
258
  titlebarTransparent: false,
245
259
  titlebarVisible: true
246
- }
260
+ },
261
+ linuxOptions: {
262
+ closable: true,
263
+ resizable: true,
264
+ minimizable: true,
265
+ titlebarTransparent: false,
266
+ titlebarVisible: true
267
+ },
268
+ allowEvaluateJS: false
247
269
 
248
270
  }) {
249
271
  super();
250
272
  this.id = ++_windowCounter;
251
- this.options = options;
273
+ this.options = { allowEvaluateJS: false, ...options };
252
274
  activeWindows.add(this);
253
275
 
254
276
  if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
@@ -261,7 +283,11 @@ class Window extends Events.EventEmitter {
261
283
  const height = options.height ? String(options.height) : "600";
262
284
 
263
285
  if(!this.options.skipCreate) {
264
- this.create(width, height, options.darwinOptions);
286
+ if (process.platform === "linux") {
287
+ this.create(width, height, options.linuxOptions || options.darwinOptions);
288
+ } else {
289
+ this.create(width, height, options.darwinOptions);
290
+ }
265
291
  }
266
292
 
267
293
  }
@@ -698,7 +724,8 @@ async request(command, ...args) {
698
724
  }
699
725
  });
700
726
 
701
- let timeout;
727
+ let timeout;
728
+
702
729
 
703
730
  if(!options.noTimeout) {
704
731
  let timeoutDuration = 7000;
@@ -712,7 +739,8 @@ if (options.timeout) {
712
739
  if (!settled) {
713
740
  settled = true;
714
741
  unsubscribe();
715
- reject(new Error(`Request timed out waiting for reply on channel "${replyChannel}"`));
742
+ // reject(new Error(`Request timed out waiting for reply on channel "${replyChannel}"`));
743
+ resolve({ error: `Request timed out waiting for reply on channel "${replyChannel}"` });
716
744
  }
717
745
  }, timeoutDuration);
718
746
  } else {
@@ -882,8 +910,16 @@ setTitlebarTransparent(isTransparent) {
882
910
  * @returns {Promise<*>} A Promise that resolves to the result of the evaluation.
883
911
  */
884
912
  async evaluateJavaScript(script) {
885
- const res = await this.request("evaluateJS", script);
886
- return res.result;
913
+ if (!this.options.allowEvaluateJS) {
914
+ throw new Error("evaluateJavaScript is disabled by default for security. Set allowEvaluateJS: true in window options to enable it.");
915
+ }
916
+ return await this.#evaluateJavaScriptInternal(script);
917
+ }
918
+
919
+
920
+ async #evaluateJavaScriptInternal(script) {
921
+ const res = await this.request("evaluateJS", script);
922
+ return res.result;
887
923
  }
888
924
 
889
925
  /**
@@ -891,7 +927,7 @@ return res.result;
891
927
  * @returns {Promise<string>} The user agent string of the window.
892
928
  */
893
929
  async getUserAgent() {
894
- return await this.evaluateJavaScript("navigator.userAgent");
930
+ return await this.#evaluateJavaScriptInternal("navigator.userAgent");
895
931
  }
896
932
 
897
933
  /**
@@ -912,7 +948,7 @@ async setStyleOf(selector, style) {
912
948
  });
913
949
  })();
914
950
  `;
915
- await this.evaluateJavaScript(script);
951
+ await this.#evaluateJavaScriptInternal(script);
916
952
  this.emit("style-updated", { selector, style });
917
953
  }
918
954
 
@@ -932,7 +968,7 @@ async setAttributeOf(selector, attribute, value) {
932
968
  });
933
969
  })();
934
970
  `;
935
- await this.evaluateJavaScript(script);
971
+ await this.#evaluateJavaScriptInternal(script);
936
972
  this.emit("attribute-updated", { selector, attribute, value });
937
973
  }
938
974
 
@@ -951,7 +987,7 @@ async removeAttributeOf(selector, attribute) {
951
987
  });
952
988
  })();
953
989
  `;
954
- await this.evaluateJavaScript(script);
990
+ await this.#evaluateJavaScriptInternal(script);
955
991
  this.emit("attribute-removed", { selector, attribute });
956
992
  }
957
993
 
@@ -973,7 +1009,7 @@ async removeStyleOf(selector, styleProperties) {
973
1009
  });
974
1010
  })();
975
1011
  `;
976
- await this.evaluateJavaScript(script);
1012
+ await this.#evaluateJavaScriptInternal(script);
977
1013
  this.emit("style-removed", { selector, styleProperties });
978
1014
  }
979
1015
 
@@ -1008,7 +1044,7 @@ async onClick(selector, channel, { replace = true } = {}) {
1008
1044
  })();
1009
1045
  `;
1010
1046
 
1011
- await this.evaluateJavaScript(script);
1047
+ await this.#evaluateJavaScriptInternal(script);
1012
1048
  }
1013
1049
 
1014
1050
  /**
@@ -1023,7 +1059,7 @@ async removeOnClick(selector) {
1023
1059
  el.onclick = null;
1024
1060
  });
1025
1061
  `;
1026
- await this.evaluateJavaScript(script);
1062
+ await this.#evaluateJavaScriptInternal(script);
1027
1063
  }
1028
1064
 
1029
1065
  /**
@@ -1033,8 +1069,8 @@ async removeOnClick(selector) {
1033
1069
  */
1034
1070
  async confirm(message) {
1035
1071
  const res = await this.request("confirm", message);
1036
- this.emit("confirm", message);
1037
- return res?.confirmed === "true";
1072
+ this.emit("confirm", res?.confirmed);
1073
+ return res?.confirmed == true || res?.confirmed === "true";
1038
1074
  }
1039
1075
 
1040
1076
  /**
@@ -1217,6 +1253,11 @@ userData: {
1217
1253
  "Application Support",
1218
1254
  process.env.POSITRON_APP_NAME
1219
1255
  );
1256
+ } else {
1257
+ // Linux / other POSIX — follow XDG Base Directory spec
1258
+ const xdgDataHome = process.env.XDG_DATA_HOME
1259
+ || path.join(process.env.HOME, ".local", "share");
1260
+ userPath = path.join(xdgDataHome, process.env.POSITRON_APP_NAME);
1220
1261
  }
1221
1262
 
1222
1263
  if(!fs.existsSync(userPath)) {
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/systemsoftware/positron.js"
6
6
  },
7
7
  "homepage": "https://positronjs.gitbook.io",
8
- "version": "1.0.6",
8
+ "version": "1.1.0",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"
@@ -19,7 +19,8 @@
19
19
  "javascript",
20
20
  "node",
21
21
  "macos",
22
- "windows"
22
+ "windows",
23
+ "linux"
23
24
  ],
24
25
  "author": "Bryce",
25
26
  "license": "MIT",