@landstrip/landstrip-api 0.18.42 → 0.18.43

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/lib/proxy.d.ts CHANGED
@@ -19,6 +19,4 @@ export interface FilterProxyHandle {
19
19
  stop(): Promise<void>;
20
20
  }
21
21
 
22
- export function isPublicProxyAddress(address: string, family?: number): boolean;
23
-
24
22
  export function startFilterProxy(options: FilterProxyOptions): Promise<FilterProxyHandle>;
package/lib/proxy.js CHANGED
@@ -2,34 +2,10 @@
2
2
  'use strict';
3
3
 
4
4
  const { lookup } = require('node:dns/promises');
5
- const { BlockList, connect, createServer, isIP } = require('node:net');
6
-
7
- const prohibitedProxyAddresses = new BlockList();
8
-
9
- for (const [network, prefix] of [
10
- ['0.0.0.0', 8],
11
- ['10.0.0.0', 8],
12
- ['100.64.0.0', 10],
13
- ['127.0.0.0', 8],
14
- ['169.254.0.0', 16],
15
- ['172.16.0.0', 12],
16
- ['192.0.0.0', 24],
17
- ['192.168.0.0', 16],
18
- ['198.18.0.0', 15],
19
- ['224.0.0.0', 4],
20
- ['240.0.0.0', 4],
21
- ]) {
22
- prohibitedProxyAddresses.addSubnet(network, prefix, 'ipv4');
23
- }
24
- for (const [network, prefix] of [
25
- ['::', 128],
26
- ['::1', 128],
27
- ['fc00::', 7],
28
- ['fe80::', 10],
29
- ['ff00::', 8],
30
- ]) {
31
- prohibitedProxyAddresses.addSubnet(network, prefix, 'ipv6');
32
- }
5
+ const { Agent, createServer: createHttpServer, request: requestHttp } = require('node:http');
6
+ const { connect, isIP } = require('node:net');
7
+
8
+ const { canonicalizeHost } = require('./shared');
33
9
 
34
10
  function parseProxyPort(value, defaultPort) {
35
11
  const rawPort = value ?? String(defaultPort);
@@ -39,56 +15,58 @@ function parseProxyPort(value, defaultPort) {
39
15
  return port >= 1 && port <= 65535 ? port : null;
40
16
  }
41
17
 
42
- function splitHostPort(target, defaultPort) {
43
- const bracketMatch = target.match(/^\[([^\]]+)\](?::(.*))?$/);
44
- const host = bracketMatch?.[1];
45
- if (host) {
46
- const port = parseProxyPort(bracketMatch[2], defaultPort);
47
- return port === null ? null : { host, port };
18
+ function splitHostPort(target) {
19
+ if (/\s|[\u0000-\u001f\u007f]/.test(target) || /[/?#\\]/.test(target)) return null;
20
+
21
+ let url;
22
+ try {
23
+ url = new URL(`connect://${target}`);
24
+ } catch {
25
+ return null;
48
26
  }
49
27
 
50
- const lastColon = target.lastIndexOf(':');
51
- if (lastColon > -1 && target.indexOf(':') === lastColon) {
52
- const port = parseProxyPort(target.slice(lastColon + 1), defaultPort);
53
- return port === null ? null : { host: target.slice(0, lastColon), port };
28
+ if (url.username || url.password || url.pathname || url.search || url.hash || !url.port) {
29
+ return null;
54
30
  }
55
31
 
56
- return { host: target, port: defaultPort };
32
+ const host = canonicalizeHost(url.hostname);
33
+ const port = parseProxyPort(url.port, 0);
34
+ if (!host || port === null) return null;
35
+ return { host, port };
57
36
  }
58
37
 
59
- function denyProxyRequest(client, status = '403 Forbidden') {
60
- client.write(`HTTP/1.1 ${status}\r\nContent-Length: 0\r\n\r\n`);
38
+ const proxyAuthenticateChallenge = 'Basic realm="landstrip"';
39
+
40
+ function denyProxyRequest(client, status = '403 Forbidden', headers = {}) {
41
+ let response = `HTTP/1.1 ${status}\r\nContent-Length: 0\r\n`;
42
+ for (const [name, value] of Object.entries(headers)) response += `${name}: ${value}\r\n`;
43
+ client.write(`${response}\r\n`);
61
44
  client.end();
62
45
  }
63
46
 
64
- function isPublicProxyAddress(address, family = isIP(address)) {
65
- if (family === 4) return !prohibitedProxyAddresses.check(address, 'ipv4');
66
- if (family === 6) return !prohibitedProxyAddresses.check(address, 'ipv6');
67
- return false;
68
- }
47
+ async function resolveProxyEndpoints(host) {
48
+ const canonicalHost = canonicalizeHost(host);
49
+ if (!canonicalHost) throw new Error('Proxy destination has an invalid host');
69
50
 
70
- async function resolveProxyEndpoint(host) {
71
- const literalFamily = isIP(host);
51
+ const literalFamily = isIP(canonicalHost);
72
52
  if (literalFamily === 4 || literalFamily === 6) {
73
- if (!isPublicProxyAddress(host, literalFamily)) {
74
- throw new Error(`Proxy destination is not public: ${host}`);
75
- }
76
- return { address: host, family: literalFamily };
53
+ return [{ address: canonicalHost, family: literalFamily }];
77
54
  }
78
55
 
79
- const addresses = await lookup(host, { all: true, verbatim: true });
80
- if (
81
- addresses.length === 0 ||
82
- addresses.some(({ address, family }) => !isPublicProxyAddress(address, family))
83
- ) {
84
- throw new Error(`Proxy destination resolves to a non-public address: ${host}`);
56
+ const addresses = await lookup(canonicalHost, { all: true, verbatim: true });
57
+ const endpoints = [];
58
+ const seen = new Set();
59
+ for (const { address } of addresses) {
60
+ const canonicalAddress = canonicalizeHost(address);
61
+ const family = canonicalAddress ? isIP(canonicalAddress) : 0;
62
+ if ((family !== 4 && family !== 6) || seen.has(canonicalAddress)) continue;
63
+ seen.add(canonicalAddress);
64
+ endpoints.push({ address: canonicalAddress, family });
85
65
  }
86
-
87
- const endpoint = addresses[0];
88
- if (endpoint.family !== 4 && endpoint.family !== 6) {
89
- throw new Error(`Proxy could not resolve destination: ${host}`);
66
+ if (endpoints.length === 0) {
67
+ throw new Error(`Proxy could not resolve destination: ${canonicalHost}`);
90
68
  }
91
- return { address: endpoint.address, family: endpoint.family };
69
+ return endpoints;
92
70
  }
93
71
 
94
72
  function pipeSockets(client, upstream, initialData) {
@@ -104,166 +82,225 @@ function pipeSockets(client, upstream, initialData) {
104
82
  function startFilterProxy(options) {
105
83
  const { isDomainAllowed, authorization, portRange } = options;
106
84
  const sockets = new Set();
85
+ const hopByHopHeaders = [
86
+ 'connection',
87
+ 'keep-alive',
88
+ 'proxy-authenticate',
89
+ 'proxy-authorization',
90
+ 'proxy-connection',
91
+ 'te',
92
+ 'trailer',
93
+ 'transfer-encoding',
94
+ 'upgrade',
95
+ ];
96
+
97
+ function trackSocket(socket) {
98
+ sockets.add(socket);
99
+ socket.once('close', () => sockets.delete(socket));
100
+ }
107
101
 
108
- // Track the upstream socket so stop() tears it down, and abandon a still-
109
- // connecting upstream when its client goes away — otherwise a connect to a
110
- // black-holed host lingers in SYN-retry (~2 min), leaking an fd per request.
111
- function trackUpstream(upstream, client, settled) {
112
- sockets.add(upstream);
113
- upstream.once('close', () => sockets.delete(upstream));
114
- client.once('close', () => {
115
- if (!settled()) upstream.destroy();
116
- });
102
+ async function connectProxyEndpoints(endpoints, port, client) {
103
+ let lastError;
104
+ for (const endpoint of endpoints) {
105
+ if (stopped || client.destroyed) throw new Error('Proxy connection was cancelled');
106
+
107
+ try {
108
+ return await new Promise((resolve, reject) => {
109
+ const upstream = connect({ host: endpoint.address, port, family: endpoint.family });
110
+ let connected = false;
111
+ const destroyUpstream = () => upstream.destroy();
112
+ trackSocket(upstream);
113
+ client.once('close', destroyUpstream);
114
+ upstream.once('connect', () => {
115
+ connected = true;
116
+ resolve(upstream);
117
+ });
118
+ upstream.once('error', reject);
119
+ upstream.once('close', () => {
120
+ client.off('close', destroyUpstream);
121
+ if (!connected) reject(new Error('Proxy connection closed'));
122
+ });
123
+ });
124
+ } catch (error) {
125
+ lastError = error;
126
+ }
127
+ }
128
+ throw lastError ?? new Error('Proxy could not connect to destination');
129
+ }
130
+
131
+ function isAuthorized(headers) {
132
+ return !authorization || headers['proxy-authorization'] === authorization;
133
+ }
134
+
135
+ function stripHopByHopHeaders(headers) {
136
+ const forwarded = { ...headers };
137
+ const connection = forwarded.connection;
138
+ const connectionValue = Array.isArray(connection) ? connection.join(',') : connection;
139
+ for (const name of connectionValue?.split(',') ?? []) {
140
+ delete forwarded[name.trim().toLowerCase()];
141
+ }
142
+ for (const name of hopByHopHeaders) delete forwarded[name];
143
+ return forwarded;
144
+ }
145
+
146
+ function denyHttpRequest(response, statusCode, statusMessage, headers = {}) {
147
+ if (!response.headersSent) {
148
+ response.writeHead(statusCode, statusMessage, {
149
+ Connection: 'close',
150
+ 'Content-Length': '0',
151
+ ...headers,
152
+ });
153
+ }
154
+ response.end();
117
155
  }
118
156
 
119
157
  async function handleConnect(client, target, rest) {
120
- const endpoint = splitHostPort(target, 443);
158
+ const endpoint = splitHostPort(target);
121
159
  if (!endpoint) {
122
160
  denyProxyRequest(client, '400 Bad Request');
123
161
  return;
124
162
  }
125
163
 
126
164
  if (!(await isDomainAllowed(endpoint.host))) {
127
- denyProxyRequest(client);
165
+ if (!client.destroyed) denyProxyRequest(client);
128
166
  return;
129
167
  }
168
+ if (stopped || client.destroyed) return;
130
169
 
131
- const resolved = await resolveProxyEndpoint(endpoint.host);
132
- let settled = false;
133
- const upstream = connect(
134
- { host: resolved.address, port: endpoint.port, family: resolved.family },
135
- () => {
136
- settled = true;
137
- client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
138
- pipeSockets(client, upstream, rest);
139
- },
140
- );
141
- trackUpstream(upstream, client, () => settled);
142
- upstream.once('error', () => {
143
- if (settled) return;
144
- settled = true;
145
- denyProxyRequest(client, '502 Bad Gateway');
146
- });
147
- }
170
+ const endpoints = await resolveProxyEndpoints(endpoint.host);
171
+ if (stopped || client.destroyed) return;
148
172
 
149
- async function handleHttp(client, headerText, rest) {
150
- const lines = headerText.split(/\r?\n/);
151
- const requestLine = lines[0];
152
- if (!requestLine) {
153
- denyProxyRequest(client, '400 Bad Request');
173
+ let upstreamSocket;
174
+ try {
175
+ upstreamSocket = await connectProxyEndpoints(endpoints, endpoint.port, client);
176
+ } catch {
177
+ if (!stopped && !client.destroyed) denyProxyRequest(client, '502 Bad Gateway');
178
+ return;
179
+ }
180
+ if (stopped || client.destroyed) {
181
+ upstreamSocket.destroy();
154
182
  return;
155
183
  }
156
184
 
157
- const [method, rawTarget, version] = requestLine.split(' ');
158
- if (!method || !rawTarget || !version) {
159
- denyProxyRequest(client, '400 Bad Request');
185
+ client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
186
+ pipeSockets(client, upstreamSocket, rest);
187
+ }
188
+
189
+ async function handleHttp(clientRequest, clientResponse) {
190
+ if (!isAuthorized(clientRequest.headers)) {
191
+ denyHttpRequest(clientResponse, 407, 'Proxy Authentication Required', {
192
+ 'Proxy-Authenticate': proxyAuthenticateChallenge,
193
+ });
160
194
  return;
161
195
  }
162
196
 
163
197
  let url;
164
198
  try {
165
- url = new URL(rawTarget);
199
+ url = new URL(clientRequest.url);
166
200
  } catch {
167
- const host = lines
168
- .find((line) => line.toLowerCase().startsWith('host:'))
169
- ?.slice(5)
170
- .trim();
171
- if (!host) {
172
- denyProxyRequest(client, '400 Bad Request');
201
+ const authority = clientRequest.headers.host;
202
+ if (!authority) {
203
+ denyHttpRequest(clientResponse, 400, 'Bad Request');
173
204
  return;
174
205
  }
175
206
 
176
207
  try {
177
- url = new URL(`http://${host}${rawTarget}`);
208
+ url = new URL(`http://${authority}${clientRequest.url}`);
178
209
  } catch {
179
- denyProxyRequest(client, '400 Bad Request');
210
+ denyHttpRequest(clientResponse, 400, 'Bad Request');
180
211
  return;
181
212
  }
182
213
  }
183
214
 
184
- if (!(await isDomainAllowed(url.hostname))) {
185
- denyProxyRequest(client);
215
+ if (url.protocol !== 'http:' || url.username || url.password) {
216
+ denyHttpRequest(clientResponse, 400, 'Bad Request');
217
+ return;
218
+ }
219
+
220
+ const host = canonicalizeHost(url.hostname);
221
+ if (!host) {
222
+ denyHttpRequest(clientResponse, 400, 'Bad Request');
223
+ return;
224
+ }
225
+ if (!(await isDomainAllowed(host))) {
226
+ if (!clientResponse.destroyed) denyHttpRequest(clientResponse, 403, 'Forbidden');
186
227
  return;
187
228
  }
229
+ const clientSocket = clientRequest.socket;
230
+ if (stopped || clientSocket.destroyed || clientResponse.destroyed) return;
188
231
 
189
- const port = parseProxyPort(url.port || undefined, url.protocol === 'https:' ? 443 : 80);
232
+ const port = parseProxyPort(url.port || undefined, 80);
190
233
  if (port === null) {
191
- denyProxyRequest(client, '400 Bad Request');
234
+ denyHttpRequest(clientResponse, 400, 'Bad Request');
192
235
  return;
193
236
  }
194
237
 
195
- const path = `${url.pathname}${url.search}` || '/';
196
- lines[0] = `${method} ${path} ${version}`;
197
-
198
- const rewrittenHeader = lines
199
- .filter(
200
- (line) =>
201
- !line.toLowerCase().startsWith('proxy-connection:') &&
202
- !line.toLowerCase().startsWith('proxy-authorization:'),
203
- )
204
- .map((line) => (line.toLowerCase().startsWith('host:') ? `Host: ${url.host}` : line))
205
- .join('\r\n');
206
- const resolved = await resolveProxyEndpoint(url.hostname);
207
- let settled = false;
208
- const upstream = connect({ host: resolved.address, port, family: resolved.family }, () => {
209
- settled = true;
210
- upstream.write(`${rewrittenHeader}\r\n\r\n`);
211
- pipeSockets(client, upstream, rest);
212
- });
213
- trackUpstream(upstream, client, () => settled);
214
- upstream.once('error', () => {
215
- if (settled) return;
216
- settled = true;
217
- denyProxyRequest(client, '502 Bad Gateway');
218
- });
219
- }
238
+ const endpoints = await resolveProxyEndpoints(host);
239
+ if (stopped || clientSocket.destroyed || clientResponse.destroyed) return;
220
240
 
221
- function handleClient(client) {
222
- sockets.add(client);
223
- client.on('close', () => sockets.delete(client));
224
- client.on('error', () => sockets.delete(client));
225
-
226
- let buffered = Buffer.alloc(0);
227
-
228
- client.on('data', (chunk) => {
229
- buffered = Buffer.concat([buffered, chunk]);
230
- const headerEnd = buffered.indexOf('\r\n\r\n');
231
- if (headerEnd === -1) {
232
- if (buffered.length > 65536) {
233
- client.removeAllListeners('data');
234
- client.pause();
235
- denyProxyRequest(client, '431 Request Header Fields Too Large');
236
- }
237
- return;
238
- }
241
+ const forwardedHeaders = stripHopByHopHeaders(clientRequest.headers);
242
+ forwardedHeaders.host = url.host;
239
243
 
240
- client.pause();
241
- client.removeAllListeners('data');
242
-
243
- const header = buffered.subarray(0, headerEnd).toString('utf-8');
244
- const rest = buffered.subarray(headerEnd + 4);
245
- if (authorization) {
246
- const supplied = header
247
- .split(/\r?\n/)
248
- .find((line) => line.toLowerCase().startsWith('proxy-authorization:'))
249
- ?.slice('proxy-authorization:'.length)
250
- .trim();
251
- if (supplied !== authorization) {
252
- denyProxyRequest(client, '407 Proxy Authentication Required');
253
- return;
254
- }
244
+ const agent = new Agent();
245
+ agent.createConnection = (_options, callback) => {
246
+ connectProxyEndpoints(endpoints, port, clientSocket).then(
247
+ (socket) => callback(null, socket),
248
+ (error) => callback(error),
249
+ );
250
+ };
251
+ const upstreamRequest = requestHttp({
252
+ host,
253
+ port,
254
+ method: clientRequest.method,
255
+ path: `${url.pathname}${url.search}` || '/',
256
+ headers: forwardedHeaders,
257
+ agent,
258
+ });
259
+ upstreamRequest.once('response', (upstreamResponse) => {
260
+ clientResponse.writeHead(
261
+ upstreamResponse.statusCode ?? 502,
262
+ upstreamResponse.statusMessage,
263
+ stripHopByHopHeaders(upstreamResponse.headers),
264
+ );
265
+ upstreamResponse.pipe(clientResponse);
266
+ });
267
+ upstreamRequest.once('error', () => {
268
+ if (!clientResponse.headersSent) {
269
+ denyHttpRequest(clientResponse, 502, 'Bad Gateway');
270
+ } else {
271
+ clientResponse.destroy();
255
272
  }
256
- const [method, target] = header.split(/\r?\n/, 1)[0].split(' ');
257
-
258
- const task =
259
- method?.toUpperCase() === 'CONNECT' && target
260
- ? handleConnect(client, target, rest)
261
- : handleHttp(client, header, rest);
262
- task.catch(() => denyProxyRequest(client, '502 Bad Gateway'));
263
273
  });
274
+ clientRequest.once('aborted', () => upstreamRequest.destroy());
275
+ clientResponse.once('close', () => {
276
+ if (!clientResponse.writableEnded) upstreamRequest.destroy();
277
+ });
278
+ clientRequest.pipe(upstreamRequest);
264
279
  }
265
280
 
266
- const server = createServer(handleClient);
281
+ const server = createHttpServer({ maxHeaderSize: 65536 }, (request, response) => {
282
+ handleHttp(request, response).catch(() => denyHttpRequest(response, 502, 'Bad Gateway'));
283
+ });
284
+ server.on('connection', trackSocket);
285
+ server.on('connect', (request, client, head) => {
286
+ if (!isAuthorized(request.headers)) {
287
+ denyProxyRequest(client, '407 Proxy Authentication Required', {
288
+ 'Proxy-Authenticate': proxyAuthenticateChallenge,
289
+ });
290
+ return;
291
+ }
292
+ handleConnect(client, request.url, head).catch(() =>
293
+ denyProxyRequest(client, '502 Bad Gateway'),
294
+ );
295
+ });
296
+ server.on('clientError', (error, client) => {
297
+ const status =
298
+ error.code === 'HPE_HEADER_OVERFLOW'
299
+ ? '431 Request Header Fields Too Large'
300
+ : '400 Bad Request';
301
+ denyProxyRequest(client, status);
302
+ });
303
+
267
304
  let stopped = false;
268
305
 
269
306
  return new Promise((resolve, reject) => {
@@ -299,6 +336,5 @@ function startFilterProxy(options) {
299
336
  }
300
337
 
301
338
  module.exports = {
302
- isPublicProxyAddress,
303
339
  startFilterProxy,
304
340
  };
package/lib/shared.d.ts CHANGED
@@ -29,6 +29,8 @@ export function sessionAllows(prefixes: Set<string>, filePath: string): boolean;
29
29
 
30
30
  export function sessionScopeFor(filePath: string, baseDirectory: string): string;
31
31
 
32
+ export function canonicalizeHost(host: string): string | null;
33
+
32
34
  export function domainMatchesPattern(domain: string, pattern: string): boolean;
33
35
 
34
36
  export function domainMatchesAny(domain: string, patterns: string[]): boolean;
package/lib/shared.js CHANGED
@@ -4,6 +4,9 @@
4
4
  const { lstatSync, readlinkSync, realpathSync } = require('node:fs');
5
5
  const { homedir } = require('node:os');
6
6
  const { basename, dirname, isAbsolute, join, relative, resolve, sep } = require('node:path');
7
+ const { domainToASCII } = require('node:url');
8
+
9
+ const ipaddr = require('ipaddr.js');
7
10
 
8
11
  function isRecord(value) {
9
12
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -64,18 +67,43 @@ function sessionScopeFor(filePath, baseDirectory) {
64
67
  return dir;
65
68
  }
66
69
 
67
- function domainMatchesPattern(domain, pattern) {
70
+ function canonicalizeHost(host) {
71
+ const bracketed = host.startsWith('[');
72
+ if (bracketed !== host.endsWith(']')) return null;
73
+
68
74
  // A trailing dot ("pastebin.com.") is the same host to DNS but would slip
69
- // past a literal deny entry; strip it from both sides before matching.
70
- const normalizedDomain = domain.toLowerCase().replace(/\.+$/, '');
71
- const normalizedPattern = pattern.toLowerCase().replace(/\.+$/, '');
72
-
73
- if (normalizedPattern === '*') return true;
74
- if (normalizedPattern.startsWith('*.')) {
75
- const base = normalizedPattern.slice(2);
76
- return normalizedDomain === base || normalizedDomain.endsWith(`.${base}`);
75
+ // past a literal deny entry; strip a single trailing dot and reject the rest.
76
+ const value = bracketed ? host.slice(1, -1) : host.replace(/\.$/, '');
77
+ if (!value || value.endsWith('.')) return null;
78
+
79
+ if (ipaddr.isValid(value)) {
80
+ if (bracketed && ipaddr.parse(value).kind() !== 'ipv6') return null;
81
+ return ipaddr.process(value).toString();
82
+ }
83
+ if (bracketed) return null;
84
+
85
+ const ascii = domainToASCII(value);
86
+ if (!ascii) return null;
87
+
88
+ try {
89
+ const parsed = new URL(`http://${ascii}/`);
90
+ return parsed.hostname === ascii.toLowerCase() ? parsed.hostname : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function domainMatchesPattern(domain, pattern) {
97
+ const normalizedDomain = canonicalizeHost(domain);
98
+ if (!normalizedDomain) return false;
99
+ if (pattern === '*') return true;
100
+
101
+ if (pattern.startsWith('*.')) {
102
+ const base = canonicalizeHost(pattern.slice(2));
103
+ return base !== null && (normalizedDomain === base || normalizedDomain.endsWith(`.${base}`));
77
104
  }
78
105
 
106
+ const normalizedPattern = canonicalizeHost(pattern);
79
107
  return normalizedDomain === normalizedPattern;
80
108
  }
81
109
 
@@ -330,6 +358,7 @@ module.exports = {
330
358
  pathUnderDirectory,
331
359
  sessionAllows,
332
360
  sessionScopeFor,
361
+ canonicalizeHost,
333
362
  domainMatchesPattern,
334
363
  domainMatchesAny,
335
364
  allowsAllDomains,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@landstrip/landstrip-api",
3
- "version": "0.18.42",
3
+ "version": "0.18.43",
4
4
  "description": "Sandbox runner using Landlock, Seatbelt, and AppContainer",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/landstrip/landstrip#readme",
@@ -36,13 +36,16 @@
36
36
  "README.md",
37
37
  "LICENSE"
38
38
  ],
39
+ "dependencies": {
40
+ "ipaddr.js": "^2.5.0"
41
+ },
39
42
  "optionalDependencies": {
40
- "@landstrip/landstrip-darwin-arm64": "0.18.42",
41
- "@landstrip/landstrip-darwin-x64": "0.18.42",
42
- "@landstrip/landstrip-linux-x64": "0.18.42",
43
- "@landstrip/landstrip-linux-arm64": "0.18.42",
44
- "@landstrip/landstrip-win32-x64": "0.18.42",
45
- "@landstrip/landstrip-win32-arm64": "0.18.42"
43
+ "@landstrip/landstrip-darwin-arm64": "0.18.43",
44
+ "@landstrip/landstrip-darwin-x64": "0.18.43",
45
+ "@landstrip/landstrip-linux-x64": "0.18.43",
46
+ "@landstrip/landstrip-linux-arm64": "0.18.43",
47
+ "@landstrip/landstrip-win32-x64": "0.18.43",
48
+ "@landstrip/landstrip-win32-arm64": "0.18.43"
46
49
  },
47
50
  "publishConfig": {
48
51
  "access": "public"