@zimic/interceptor 1.5.0 → 1.5.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.
package/dist/server.js CHANGED
@@ -1,29 +1,29 @@
1
1
  'use strict';
2
2
 
3
- var chunk3W3A4TJ5_js = require('./chunk-3W3A4TJ5.js');
3
+ var chunk3WCEMV5U_js = require('./chunk-3WCEMV5U.js');
4
4
  require('./chunk-DGUM43GV.js');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "DEFAULT_ACCESS_CONTROL_HEADERS", {
9
9
  enumerable: true,
10
- get: function () { return chunk3W3A4TJ5_js.DEFAULT_ACCESS_CONTROL_HEADERS; }
10
+ get: function () { return chunk3WCEMV5U_js.DEFAULT_ACCESS_CONTROL_HEADERS; }
11
11
  });
12
12
  Object.defineProperty(exports, "DEFAULT_PREFLIGHT_STATUS_CODE", {
13
13
  enumerable: true,
14
- get: function () { return chunk3W3A4TJ5_js.DEFAULT_PREFLIGHT_STATUS_CODE; }
14
+ get: function () { return chunk3WCEMV5U_js.DEFAULT_PREFLIGHT_STATUS_CODE; }
15
15
  });
16
16
  Object.defineProperty(exports, "NotRunningInterceptorServerError", {
17
17
  enumerable: true,
18
- get: function () { return chunk3W3A4TJ5_js.NotRunningInterceptorServerError_default; }
18
+ get: function () { return chunk3WCEMV5U_js.NotRunningInterceptorServerError_default; }
19
19
  });
20
20
  Object.defineProperty(exports, "RunningInterceptorServerError", {
21
21
  enumerable: true,
22
- get: function () { return chunk3W3A4TJ5_js.RunningInterceptorServerError_default; }
22
+ get: function () { return chunk3WCEMV5U_js.RunningInterceptorServerError_default; }
23
23
  });
24
24
  Object.defineProperty(exports, "createInterceptorServer", {
25
25
  enumerable: true,
26
- get: function () { return chunk3W3A4TJ5_js.createInterceptorServer; }
26
+ get: function () { return chunk3WCEMV5U_js.createInterceptorServer; }
27
27
  });
28
28
  //# sourceMappingURL=server.js.map
29
29
  //# sourceMappingURL=server.js.map
package/dist/server.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default as NotRunningInterceptorServerError, RunningInterceptorServerError_default as RunningInterceptorServerError, createInterceptorServer } from './chunk-F3VMDYHM.mjs';
1
+ export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default as NotRunningInterceptorServerError, RunningInterceptorServerError_default as RunningInterceptorServerError, createInterceptorServer } from './chunk-YQNDAJTN.mjs';
2
2
  import './chunk-BJTO5JO5.mjs';
3
3
  //# sourceMappingURL=server.mjs.map
4
4
  //# sourceMappingURL=server.mjs.map
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "api",
15
15
  "static"
16
16
  ],
17
- "version": "1.5.0",
17
+ "version": "1.5.1",
18
18
  "homepage": "https://zimic.dev/docs/interceptor",
19
19
  "repository": {
20
20
  "type": "git",
@@ -100,12 +100,12 @@
100
100
  "isomorphic-ws": "5.0.0",
101
101
  "msw": "2.15.0",
102
102
  "picocolors": "^1.1.1",
103
- "ws": "8.21.0",
104
- "yargs": "18.0.0",
103
+ "ws": "8.21.1",
104
+ "yargs": "18.1.0",
105
105
  "zod": "^4.4.3"
106
106
  },
107
107
  "devDependencies": {
108
- "@types/node": "^26.1.1",
108
+ "@types/node": "^26.1.2",
109
109
  "@types/ws": "^8.18.1",
110
110
  "@types/yargs": "^17.0.35",
111
111
  "@typescript/native": "npm:typescript@^7.0.2",
@@ -116,10 +116,10 @@
116
116
  "@zimic/lint-staged-config": "workspace:*",
117
117
  "@zimic/tsconfig": "workspace:*",
118
118
  "@zimic/utils": "workspace:*",
119
- "concurrently": "^10.0.3",
119
+ "concurrently": "^10.0.4",
120
120
  "dotenv-cli": "^11.0.0",
121
- "eslint": "^9.39.4",
122
- "playwright": "^1.61.1",
121
+ "eslint": "^9.39.5",
122
+ "playwright": "^1.62.0",
123
123
  "tsup": "^8.5.1",
124
124
  "typescript": "npm:@typescript/typescript6@^6.0.2",
125
125
  "vitest": "^4.1.10"
package/src/cli/cli.ts CHANGED
@@ -79,9 +79,9 @@ async function runCLI() {
79
79
  type: 'string',
80
80
  description:
81
81
  'The directory where the authorized interceptor authentication tokens are saved. If provided, only ' +
82
- 'remote interceptors bearing a valid token will be accepted. This option is essential if you are ' +
83
- 'exposing your interceptor server publicly. For local development and testing, though, ' +
84
- '`--tokens-dir` is optional.',
82
+ 'remote interceptors bearing a valid token will be accepted. While authentication is optional for ' +
83
+ 'private development servers, it is strongly recommended when the server is not bound to localhost, ' +
84
+ '127.0.0.1, or ::1, or when it is shared over a network.',
85
85
  alias: 't',
86
86
  }),
87
87
  async (cliArguments) => {
@@ -42,6 +42,7 @@ async function startInterceptorServer({
42
42
  }
43
43
 
44
44
  const exitCode = exitEvent ? PROCESS_EXIT_CODE_BY_EXIT_EVENT[exitEvent] : undefined;
45
+
45
46
  if (exitCode !== undefined) {
46
47
  process.exit(exitCode);
47
48
  }
@@ -64,18 +65,6 @@ async function startInterceptorServer({
64
65
  `${ephemeral ? 'Ephemeral s' : 'S'}erver is running on ${color.yellow(`${server.hostname}:${server.port}`)}`,
65
66
  );
66
67
 
67
- const isDangerouslyUnprotected = !tokensDirectory && process.env.NODE_ENV === 'production';
68
-
69
- if (isDangerouslyUnprotected) {
70
- logger.warn(
71
- [
72
- `Attention: this interceptor server is ${color.bold(color.red('unprotected'))}. Do not expose it publicly without authentication.`,
73
- '',
74
- 'Learn more: https://zimic.dev/docs/interceptor/guides/http/remote-interceptors#interceptor-server-authentication',
75
- ].join('\n'),
76
- );
77
- }
78
-
79
68
  if (onReady) {
80
69
  try {
81
70
  await runCommand(onReady.command, onReady.arguments);
@@ -90,8 +90,7 @@ class HttpInterceptorImplementation<
90
90
  };
91
91
 
92
92
  this.onUnhandledRequest = options.onUnhandledRequest satisfies
93
- | UnhandledRequestStrategy
94
- | undefined as this['onUnhandledRequest'];
93
+ UnhandledRequestStrategy | undefined as this['onUnhandledRequest'];
95
94
 
96
95
  this.Handler = options.Handler;
97
96
  }
@@ -249,8 +249,7 @@ abstract class HttpInterceptorWorker {
249
249
  createResponseFromDeclaration(
250
250
  request: HttpRequest,
251
251
  declaration:
252
- | { status: number; headers?: HttpHeadersInit; body?: HttpBody }
253
- | { action: UnhandledRequestStrategy.Action },
252
+ { status: number; headers?: HttpHeadersInit; body?: HttpBody } | { action: UnhandledRequestStrategy.Action },
254
253
  ): PossiblePromise<HttpResponse | null> {
255
254
  if ('action' in declaration) {
256
255
  if (declaration.action === 'bypass') {
@@ -252,8 +252,7 @@ class LocalHttpInterceptorWorker extends HttpInterceptorWorker {
252
252
  async createResponseFromDeclaration(
253
253
  request: HttpRequest,
254
254
  declaration:
255
- | { status: number; headers?: HttpHeadersInit; body?: HttpBody }
256
- | { action: UnhandledRequestStrategy.Action },
255
+ { status: number; headers?: HttpHeadersInit; body?: HttpBody } | { action: UnhandledRequestStrategy.Action },
257
256
  ) {
258
257
  const requestClone = request.clone();
259
258
  const response = await super.createResponseFromDeclaration(request, declaration);
@@ -171,8 +171,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
171
171
  async createResponseFromDeclaration(
172
172
  request: HttpRequest,
173
173
  declaration:
174
- | { status: number; headers?: HttpHeadersInit; body?: HttpBody }
175
- | { action: UnhandledRequestStrategy.Action },
174
+ { status: number; headers?: HttpHeadersInit; body?: HttpBody } | { action: UnhandledRequestStrategy.Action },
176
175
  ) {
177
176
  const response = await super.createResponseFromDeclaration(request, declaration);
178
177
 
@@ -4,10 +4,13 @@ import { startHttpServer, stopHttpServer, getHttpServerPort } from '@zimic/utils
4
4
  import { createRegexFromPath, excludeNonPathParams } from '@zimic/utils/url';
5
5
  import { createServer, Server as HttpServer, IncomingMessage, ServerResponse } from 'http';
6
6
  import type { WebSocket as Socket } from 'isomorphic-ws';
7
+ import color from 'picocolors';
7
8
 
8
9
  import HttpInterceptorWorker from '@/http/interceptorWorker/HttpInterceptorWorker';
9
10
  import { removeArrayIndex } from '@/utils/arrays';
10
11
  import { deserializeResponse, SerializedHttpRequest, serializeRequest } from '@/utils/fetch';
12
+ import { isLoopbackHostname } from '@/utils/http';
13
+ import { logger } from '@/utils/logging';
11
14
  import { WebSocketMessageAbortError } from '@/utils/webSocket';
12
15
  import { WebSocketEventMessage } from '@/utils/webSocket/types';
13
16
  import WebSocketServer, { WebSocketServerAuthenticate } from '@/utils/webSocket/WebSocketServer';
@@ -124,11 +127,58 @@ class InterceptorServer implements PublicInterceptorServer {
124
127
  });
125
128
 
126
129
  this.startWebSocketServer();
130
+
131
+ const isDangerouslyUnprotected =
132
+ !this.tokensDirectory && (process.env.NODE_ENV === 'production' || !isLoopbackHostname(this.hostname));
133
+
134
+ if (isDangerouslyUnprotected) {
135
+ logger.warn(
136
+ [
137
+ `Attention: this interceptor server is ${color.bold(
138
+ color.red('unprotected'),
139
+ )}. Do not expose it publicly without authentication.`,
140
+ '',
141
+ 'For your safety, this server will reject remote browser interceptors until authentication is configured.',
142
+ '',
143
+ 'In @zimic/interceptor v2, interceptor servers running on non-loopback hostnames will require ' +
144
+ 'authentication and refuse to start without a tokens directory.',
145
+ '',
146
+ 'Learn more: https://zimic.dev/docs/interceptor/guides/http/remote-interceptors#interceptor-server-authentication',
147
+ ].join('\n'),
148
+ );
149
+ }
127
150
  }
128
151
 
129
152
  private authenticateWebSocketConnection: WebSocketServerAuthenticate = async (_socket, request) => {
130
153
  if (!this.tokensDirectory) {
131
- return { isValid: true };
154
+ // Requests without an origin header are allowed when the interceptor server is not configured to require token
155
+ // authentication. They are typically made by non-browser clients.
156
+ if (request.headers.origin === undefined) {
157
+ return { isValid: true };
158
+ }
159
+
160
+ try {
161
+ const originURL = new URL(request.headers.origin);
162
+ const originHostname = originURL.hostname === '[::1]' ? '::1' : originURL.hostname;
163
+
164
+ const originMatchesServerLoopback =
165
+ originURL.origin === request.headers.origin &&
166
+ isLoopbackHostname(this.hostname) &&
167
+ isLoopbackHostname(originHostname);
168
+
169
+ if (originMatchesServerLoopback) {
170
+ return { isValid: true };
171
+ }
172
+ } catch (error) {
173
+ console.error(error);
174
+ }
175
+
176
+ return {
177
+ isValid: false,
178
+ message:
179
+ 'Unauthenticated browser connections are only allowed from loopback origins. ' +
180
+ 'Configure token authentication.',
181
+ };
132
182
  }
133
183
 
134
184
  const tokenValue = this.getWebSocketRequestTokenValue(request);
package/src/utils/http.ts CHANGED
@@ -14,3 +14,9 @@ export const HTTP_METHODS_WITH_RESPONSE_BODY = new Set<HttpMethod>([
14
14
  export function methodCanHaveResponseBody(method: HttpMethod) {
15
15
  return HTTP_METHODS_WITH_RESPONSE_BODY.has(method);
16
16
  }
17
+
18
+ export const LOOPBACK_HOSTNAMES = Object.freeze(['localhost', '127.0.0.1', '::1']);
19
+
20
+ export function isLoopbackHostname(hostname: string) {
21
+ return LOOPBACK_HOSTNAMES.includes(hostname.toLowerCase());
22
+ }
@@ -58,5 +58,4 @@ export type WebSocketReplyMessageListener<Schema extends WebSocketSchema, Channe
58
58
  ) => PossiblePromise<void>;
59
59
 
60
60
  export type WebSocketMessageListener<Schema extends WebSocketSchema, Channel extends WebSocketChannel<Schema>> =
61
- | WebSocketEventMessageListener<Schema, Channel>
62
- | WebSocketReplyMessageListener<Schema, Channel>;
61
+ WebSocketEventMessageListener<Schema, Channel> | WebSocketReplyMessageListener<Schema, Channel>;