@verdant-web/server 2.0.7 → 2.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.
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  import { ServerMessage } from '@verdant-web/common';
2
3
  import { IncomingMessage, ServerResponse } from 'http';
3
4
  import { WebSocket } from 'ws';
@@ -7,6 +8,7 @@ export declare class ClientConnectionManager {
7
8
  private getLibraryConnections;
8
9
  addSocket: (libraryId: string, key: string, socket: WebSocket, info: TokenInfo) => void;
9
10
  addRequest: (libraryId: string, key: string, request: IncomingMessage, response: ServerResponse, info: TokenInfo) => () => void;
11
+ addFetch: (libraryId: string, key: string, req: Request, info: TokenInfo) => (() => Response);
10
12
  remove: (libraryId: string, key: string) => void;
11
13
  respond: (libraryId: string, key: string, message: ServerMessage) => void;
12
14
  /**
@@ -28,6 +28,29 @@ class RequestClientConnection extends ClientConnection {
28
28
  };
29
29
  }
30
30
  }
31
+ class FetchClientConnection extends ClientConnection {
32
+ constructor(key, req, info) {
33
+ super(key);
34
+ this.req = req;
35
+ this.info = info;
36
+ this.type = 'fetch';
37
+ this.responses = [];
38
+ this.res = new Response('', { status: 200 });
39
+ this.respond = (message) => {
40
+ this.responses.push(message);
41
+ };
42
+ this.end = () => {
43
+ this.res = new Response(JSON.stringify({
44
+ messages: this.responses,
45
+ }), {
46
+ status: 200,
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ },
50
+ });
51
+ };
52
+ }
53
+ }
31
54
  class WebSocketClientConnection extends ClientConnection {
32
55
  constructor(key, ws, info) {
33
56
  super(key);
@@ -65,6 +88,16 @@ class ClientConnectionManager {
65
88
  lib.delete(key);
66
89
  };
67
90
  };
91
+ this.addFetch = (libraryId, key, req, info) => {
92
+ const lib = this.getLibraryConnections(libraryId);
93
+ const connection = new FetchClientConnection(key, req, info);
94
+ lib.set(key, connection);
95
+ return () => {
96
+ connection.end();
97
+ lib.delete(key);
98
+ return connection.res;
99
+ };
100
+ };
68
101
  this.remove = (libraryId, key) => {
69
102
  const lib = this.getLibraryConnections(libraryId);
70
103
  lib.delete(key);
@@ -1 +1 @@
1
- {"version":3,"file":"ClientConnection.js","sourceRoot":"","sources":["../../src/ClientConnection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ClientConnection.js","sourceRoot":"","sources":["../../src/ClientConnection.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  /// <reference types="node" resolution-mode="require"/>
3
+ /// <reference types="node" resolution-mode="require"/>
4
+ /// <reference types="node" resolution-mode="require"/>
3
5
  import { DocumentBaseline, Operation, ServerMessage } from '@verdant-web/common';
4
6
  import EventEmitter from 'events';
5
7
  import { IncomingMessage, Server as HttpServer, ServerResponse } from 'http';
@@ -7,6 +9,7 @@ import { UserProfiles } from './Profiles.js';
7
9
  import { MessageSender } from './MessageSender.js';
8
10
  import { TokenInfo } from './TokenVerifier.js';
9
11
  import { FileStorage } from './files/FileStorage.js';
12
+ import internal from 'stream';
10
13
  import { FileMetadataConfig } from './files/FileMetadata.js';
11
14
  export interface ServerOptions {
12
15
  /**
@@ -68,7 +71,7 @@ export declare interface Server {
68
71
  emit<Event extends keyof ServerEvents>(ev: Event, ...args: Parameters<ServerEvents[Event]>): boolean;
69
72
  }
70
73
  export declare class Server extends EventEmitter implements MessageSender {
71
- readonly httpServer: HttpServer;
74
+ private httpServer;
72
75
  private wss;
73
76
  private fileStorage?;
74
77
  private fileMetadata;
@@ -80,17 +83,50 @@ export declare class Server extends EventEmitter implements MessageSender {
80
83
  private log;
81
84
  private __testMode;
82
85
  constructor(options: ServerOptions);
86
+ /**
87
+ * Attaches the Verdant server to an HttpServer instance, which
88
+ * allows it to handle requests and websockets required for the
89
+ * Verdant protocol.
90
+ *
91
+ * You can pass httpPath = false if you want to handle HTTP requests
92
+ * yourself, and this will only attach the websocket handling.
93
+ */
94
+ attach: (server: HttpServer, options?: {
95
+ httpPath?: string | false;
96
+ }) => void;
97
+ /**
98
+ * Handles an HTTP upgrade request to a websocket connection.
99
+ */
100
+ handleUpgrade: (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => Promise<void>;
83
101
  private authorizeRequest;
84
102
  private getRequestToken;
85
- private internalServerHandleRequest;
103
+ private createInternalRequestHandler;
104
+ /**
105
+ * Handles an HTTP request from a verdant client.
106
+ */
86
107
  handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
87
- private sendErrorResponse;
108
+ /**
109
+ * Handles a "fetch" style request. Complement to handleRequest, for servers
110
+ * that use Request/Response style handlers.
111
+ */
112
+ handleFetch: (req: Request) => Promise<Response>;
113
+ private handleRequestBody;
114
+ private writeErrorResponse;
115
+ private getErrorResponse;
88
116
  /**
89
117
  * Handles a multipart upload of a file from a verdant client. The upload
90
118
  * will include parameters for the file's ID, name, and type. The request
91
119
  * must be authenticated with a token to tie it to a library.
92
120
  */
93
121
  handleFileRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
122
+ /**
123
+ * Handles a "fetch" style file request. Complement to handleFileRequest,
124
+ * for servers that use Request/Response style handlers.
125
+ */
126
+ handleFileFetch: (req: Request) => Promise<Response>;
127
+ private getFileStorageOrThrow;
128
+ private streamIncomingFile;
129
+ private getFileData;
94
130
  broadcast: (libraryId: string, message: ServerMessage, omitKeys?: string[]) => void;
95
131
  send: (libraryId: string, key: string, message: ServerMessage) => void;
96
132
  private handleMessage;
@@ -30,6 +30,7 @@ const stream_1 = require("stream");
30
30
  const FileMetadata_js_1 = require("./files/FileMetadata.js");
31
31
  const ServerLibrary_js_1 = require("./ServerLibrary.js");
32
32
  const migrations_js_1 = require("./migrations.js");
33
+ const web_1 = require("node:stream/web");
33
34
  class DefaultProfiles {
34
35
  constructor() {
35
36
  this.get = async (userId) => {
@@ -45,10 +46,58 @@ class Server extends events_1.default {
45
46
  this.connectionToReplicaIdMap = new WeakMap();
46
47
  this.log = () => { };
47
48
  this.__testMode = false;
49
+ /**
50
+ * Attaches the Verdant server to an HttpServer instance, which
51
+ * allows it to handle requests and websockets required for the
52
+ * Verdant protocol.
53
+ *
54
+ * You can pass httpPath = false if you want to handle HTTP requests
55
+ * yourself, and this will only attach the websocket handling.
56
+ */
57
+ this.attach = (server, options = {}) => {
58
+ if (this.httpServer) {
59
+ this.httpServer.off('upgrade', this.handleUpgrade);
60
+ }
61
+ this.httpServer = server;
62
+ this.httpServer.on('upgrade', this.handleUpgrade);
63
+ if (options.httpPath !== false) {
64
+ this.httpServer.on('request', this.createInternalRequestHandler(options.httpPath));
65
+ }
66
+ };
67
+ /**
68
+ * Handles an HTTP upgrade request to a websocket connection.
69
+ */
70
+ this.handleUpgrade = async (req, socket, head) => {
71
+ try {
72
+ const info = this.authorizeRequest(req);
73
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
74
+ this.wss.emit('connection', ws, req, info);
75
+ });
76
+ }
77
+ catch (e) {
78
+ this.emit('error', e);
79
+ if (e instanceof common_1.VerdantError && e.httpStatus === 401) {
80
+ socket.write('HTTP/1.1 401 Unauthorized\r\n' +
81
+ 'Connection: close\r\n' +
82
+ 'Content-Length: 0\r\n' +
83
+ '\r\n');
84
+ }
85
+ socket.destroy();
86
+ }
87
+ };
48
88
  this.authorizeRequest = (req) => {
49
89
  return this.tokenVerifier.verifyToken(this.getRequestToken(req));
50
90
  };
51
91
  this.getRequestToken = (req) => {
92
+ if (isFetch(req)) {
93
+ const authHeader = req.headers.get('Authorization');
94
+ (0, common_1.assert)(authHeader, 'Token is required');
95
+ const [type, token] = authHeader.split(' ');
96
+ if (type === 'Bearer') {
97
+ return token;
98
+ }
99
+ return token;
100
+ }
52
101
  if (req.headers.authorization) {
53
102
  const [type, token] = req.headers.authorization.split(' ');
54
103
  if (type === 'Bearer') {
@@ -68,21 +117,27 @@ class Server extends events_1.default {
68
117
  (0, common_1.assert)(token, 'Token is required');
69
118
  return token;
70
119
  };
71
- this.internalServerHandleRequest = (req, res) => {
72
- const url = new url_1.URL(req.url || '', 'http://localhost');
73
- if (url.pathname.startsWith('lofi')) {
74
- if (url.pathname === 'lofi') {
75
- return this.handleRequest(req, res);
120
+ this.createInternalRequestHandler = (pathPrefix = '/sync') => {
121
+ const handleRequest = (req, res) => {
122
+ const url = new url_1.URL(req.url || '', 'http://localhost');
123
+ if (url.pathname.startsWith('sync')) {
124
+ if (url.pathname === 'sync') {
125
+ return this.handleRequest(req, res);
126
+ }
127
+ else if (url.pathname.startsWith('/sync/files/')) {
128
+ return this.handleFileRequest(req, res);
129
+ }
76
130
  }
77
- else if (url.pathname.startsWith('/lofi/files/')) {
78
- return this.handleFileRequest(req, res);
131
+ else {
132
+ res.writeHead(404);
133
+ res.end();
79
134
  }
80
- }
81
- else {
82
- res.writeHead(404);
83
- res.end();
84
- }
135
+ };
136
+ return handleRequest;
85
137
  };
138
+ /**
139
+ * Handles an HTTP request from a verdant client.
140
+ */
86
141
  this.handleRequest = async (req, res) => {
87
142
  var _a;
88
143
  try {
@@ -104,28 +159,45 @@ class Server extends events_1.default {
104
159
  reject(e);
105
160
  });
106
161
  }));
107
- try {
108
- for (const message of body.messages) {
109
- await this.handleMessage(key, info, message);
110
- }
111
- }
112
- catch (e) {
113
- this.emit('error', e);
114
- res.writeHead(500);
115
- res.end();
116
- return;
117
- }
118
- // update our keepalive timers for presence management
119
- const firstMessage = body.messages[0];
120
- if (firstMessage) {
121
- this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
122
- }
162
+ await this.handleRequestBody(key, info, body);
123
163
  finish();
124
164
  this.emit('request', info);
125
165
  }
126
166
  }
127
167
  catch (e) {
128
- return this.sendErrorResponse(e, res);
168
+ return this.writeErrorResponse(e, res);
169
+ }
170
+ };
171
+ /**
172
+ * Handles a "fetch" style request. Complement to handleRequest, for servers
173
+ * that use Request/Response style handlers.
174
+ */
175
+ this.handleFetch = async (req) => {
176
+ try {
177
+ const info = this.authorizeRequest(req);
178
+ const key = (0, common_1.generateId)();
179
+ const finish = this.clientConnections.addFetch(info.libraryId, key, req, info);
180
+ const body = (await req.json());
181
+ if (!body) {
182
+ throw new common_1.VerdantError(common_1.VerdantError.Code.BodyRequired);
183
+ }
184
+ await this.handleRequestBody(key, info, body);
185
+ const res = finish();
186
+ this.emit('request', info);
187
+ return res;
188
+ }
189
+ catch (e) {
190
+ return this.getErrorResponse(e);
191
+ }
192
+ };
193
+ this.handleRequestBody = async (key, info, body) => {
194
+ for (const message of body.messages) {
195
+ await this.handleMessage(key, info, message);
196
+ }
197
+ // update our keepalive timers for presence management
198
+ const firstMessage = body.messages[0];
199
+ if (firstMessage) {
200
+ this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
129
201
  }
130
202
  };
131
203
  /**
@@ -134,70 +206,19 @@ class Server extends events_1.default {
134
206
  * must be authenticated with a token to tie it to a library.
135
207
  */
136
208
  this.handleFileRequest = async (req, res) => {
137
- const fs = this.fileStorage;
138
- if (!fs) {
139
- this.emit('error', new Error('No file storage configured, but a client attempted to upload a file.'));
140
- res.writeHead(500);
141
- res.write('File storage is not configured');
142
- res.end();
143
- return;
144
- }
145
- // FIXME: rather than trying to support Express, I should
146
- // just expose regular methods you call from whatever HTTP handler...
147
- const url = new url_1.URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
148
- const id = url.pathname.split('/').pop();
149
- if (!id || id === 'files') {
150
- res.writeHead(400);
151
- res.write('File ID is required to be in the URL path as the last parameter');
152
- res.end();
153
- return;
154
- }
155
209
  try {
210
+ const info = this.authorizeRequest(req);
211
+ const url = new url_1.URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
212
+ const id = url.pathname.split('/').pop();
213
+ if (!id || id === 'files') {
214
+ throw new common_1.VerdantError(common_1.VerdantError.Code.NotFound);
215
+ }
156
216
  if (req.method === 'POST') {
157
- const info = this.authorizeRequest(req);
158
- await new Promise((resolve, reject) => {
159
- const bb = (0, busboy_1.default)({ headers: req.headers });
160
- bb.on('file', (fieldName, stream, fileInfo) => {
161
- // too many 'info's....
162
- const lofiFileInfo = {
163
- id,
164
- libraryId: info.libraryId,
165
- fileName: fileInfo.filename,
166
- type: fileInfo.mimeType,
167
- };
168
- // write metadata to storage
169
- try {
170
- this.fileMetadata.put(info.libraryId, lofiFileInfo);
171
- fs.put(stream, lofiFileInfo);
172
- }
173
- catch (e) {
174
- reject(e);
175
- }
176
- });
177
- bb.on('field', (fieldName, value) => {
178
- if (fieldName === 'file') {
179
- if (this.__testMode) {
180
- // this isn't right in the real world, but in testing it's
181
- // the only way we get file data.
182
- // we create a stream from the data and pass it as if it
183
- // were a file stream
184
- const stream = new stream_1.Readable();
185
- stream.push(value);
186
- stream.push(null);
187
- const fileInfo = {
188
- filename: 'test.txt',
189
- mimeType: 'text/plain',
190
- };
191
- bb.emit('file', fieldName, stream, fileInfo);
192
- }
193
- else {
194
- throw new Error('Invalid file upload');
195
- }
196
- }
197
- });
198
- req.pipe(bb);
199
- bb.on('finish', resolve);
200
- bb.on('error', reject);
217
+ await this.streamIncomingFile({
218
+ req,
219
+ info,
220
+ headers: req.headers,
221
+ id,
201
222
  });
202
223
  this.log('File upload complete');
203
224
  res.writeHead(200);
@@ -205,39 +226,147 @@ class Server extends events_1.default {
205
226
  res.end();
206
227
  }
207
228
  else if (req.method === 'GET') {
208
- const info = this.authorizeRequest(req);
209
- const fileInfo = this.fileMetadata.get(info.libraryId, id);
210
- if (!fileInfo) {
211
- res.writeHead(404);
212
- res.end();
213
- return;
214
- }
215
- const url = await fs.getUrl({
216
- fileName: fileInfo.name,
217
- id: fileInfo.fileId,
218
- libraryId: info.libraryId,
219
- type: fileInfo.type,
220
- });
229
+ const data = await this.getFileData(info, id);
221
230
  res.writeHead(200, {
222
231
  'Content-Type': 'application/json',
223
232
  });
224
- // we need to augment that data with the URL from the file backend.
225
- // and generally enforce the FileData interface here...
226
- const data = {
227
- id: fileInfo.fileId,
228
- url,
229
- remote: true,
230
- name: fileInfo.name,
231
- type: fileInfo.type,
232
- };
233
233
  res.write(JSON.stringify(data));
234
234
  res.end();
235
235
  }
236
236
  }
237
237
  catch (e) {
238
- return this.sendErrorResponse(e, res);
238
+ return this.writeErrorResponse(e, res);
239
239
  }
240
240
  };
241
+ /**
242
+ * Handles a "fetch" style file request. Complement to handleFileRequest,
243
+ * for servers that use Request/Response style handlers.
244
+ */
245
+ this.handleFileFetch = async (req) => {
246
+ this.log('info', 'Handling file fetch', req.url, req.method);
247
+ try {
248
+ const info = this.authorizeRequest(req);
249
+ const url = new url_1.URL(req.url, 'http://localhost');
250
+ const id = url.pathname.split('/').pop();
251
+ if (!id || id === 'files') {
252
+ throw new common_1.VerdantError(common_1.VerdantError.Code.NotFound);
253
+ }
254
+ if (req.method === 'POST') {
255
+ if (!req.body) {
256
+ throw new common_1.VerdantError(common_1.VerdantError.Code.InvalidRequest);
257
+ }
258
+ const headersAsRecord = Array.from(req.headers.entries()).reduce((acc, [key, value]) => {
259
+ acc[key] = value;
260
+ return acc;
261
+ }, {});
262
+ // this is needed because Node's webstreams don't
263
+ // like itty's polyfill streams
264
+ const intermediate = new web_1.TransformStream();
265
+ req.body.pipeTo(intermediate.writable);
266
+ await this.streamIncomingFile({
267
+ req: stream_1.Readable.fromWeb(intermediate.readable),
268
+ info,
269
+ headers: headersAsRecord,
270
+ id,
271
+ });
272
+ this.log('File upload complete');
273
+ return new Response(JSON.stringify({ success: true }), {
274
+ status: 200,
275
+ headers: {
276
+ 'Content-Type': 'application/json',
277
+ },
278
+ });
279
+ }
280
+ else if (req.method === 'GET') {
281
+ const data = await this.getFileData(info, id);
282
+ return new Response(JSON.stringify(data), {
283
+ status: 200,
284
+ headers: {
285
+ 'Content-Type': 'application/json',
286
+ },
287
+ });
288
+ }
289
+ else {
290
+ throw new common_1.VerdantError(common_1.VerdantError.Code.InvalidRequest);
291
+ }
292
+ }
293
+ catch (e) {
294
+ return this.getErrorResponse(e);
295
+ }
296
+ };
297
+ this.getFileStorageOrThrow = () => {
298
+ if (!this.fileStorage) {
299
+ throw new common_1.VerdantError(common_1.VerdantError.Code.NoFileStorage);
300
+ }
301
+ return this.fileStorage;
302
+ };
303
+ this.streamIncomingFile = ({ id, req, headers, info, }) => {
304
+ const fs = this.getFileStorageOrThrow();
305
+ return new Promise((resolve, reject) => {
306
+ const bb = (0, busboy_1.default)({ headers });
307
+ bb.on('file', (fieldName, stream, fileInfo) => {
308
+ // too many 'info's....
309
+ const lofiFileInfo = {
310
+ id,
311
+ libraryId: info.libraryId,
312
+ fileName: fileInfo.filename,
313
+ type: fileInfo.mimeType,
314
+ };
315
+ // write metadata to storage
316
+ try {
317
+ this.fileMetadata.put(info.libraryId, lofiFileInfo);
318
+ fs.put(stream, lofiFileInfo);
319
+ }
320
+ catch (e) {
321
+ reject(e);
322
+ }
323
+ });
324
+ bb.on('field', (fieldName, value) => {
325
+ if (fieldName === 'file') {
326
+ if (this.__testMode) {
327
+ // this isn't right in the real world, but in testing it's
328
+ // the only way we get file data.
329
+ // we create a stream from the data and pass it as if it
330
+ // were a file stream
331
+ const stream = new stream_1.Readable();
332
+ stream.push(value);
333
+ stream.push(null);
334
+ const fileInfo = {
335
+ filename: 'test.txt',
336
+ mimeType: 'text/plain',
337
+ };
338
+ bb.emit('file', fieldName, stream, fileInfo);
339
+ }
340
+ else {
341
+ throw new Error('Invalid file upload');
342
+ }
343
+ }
344
+ });
345
+ req.pipe(bb);
346
+ bb.on('finish', resolve);
347
+ bb.on('error', reject);
348
+ });
349
+ };
350
+ this.getFileData = async (info, id) => {
351
+ const fs = this.getFileStorageOrThrow();
352
+ const fileInfo = this.fileMetadata.get(info.libraryId, id);
353
+ if (!fileInfo) {
354
+ throw new common_1.VerdantError(common_1.VerdantError.Code.NotFound, undefined, `File ${id} not found`);
355
+ }
356
+ const url = await fs.getUrl({
357
+ fileName: fileInfo.name,
358
+ id: fileInfo.fileId,
359
+ libraryId: info.libraryId,
360
+ type: fileInfo.type,
361
+ });
362
+ return {
363
+ id: fileInfo.fileId,
364
+ url,
365
+ remote: true,
366
+ name: fileInfo.name,
367
+ type: fileInfo.type,
368
+ };
369
+ };
241
370
  this.broadcast = (libraryId, message, omitKeys = []) => {
242
371
  this.clientConnections.broadcast(libraryId, message, omitKeys);
243
372
  };
@@ -372,28 +501,10 @@ class Server extends events_1.default {
372
501
  });
373
502
  this.wss.on('connection', this.handleConnection);
374
503
  this.httpServer =
375
- options.httpServer || new http_1.Server(this.internalServerHandleRequest);
376
- this.httpServer.on('upgrade', async (req, socket, head) => {
377
- try {
378
- const info = this.authorizeRequest(req);
379
- this.wss.handleUpgrade(req, socket, head, (ws) => {
380
- this.wss.emit('connection', ws, req, info);
381
- });
382
- }
383
- catch (e) {
384
- this.emit('error', e);
385
- if (e instanceof common_1.VerdantError && e.httpStatus === 401) {
386
- socket.write('HTTP/1.1 401 Unauthorized\r\n' +
387
- 'Connection: close\r\n' +
388
- 'Content-Length: 0\r\n' +
389
- '\r\n');
390
- }
391
- socket.destroy();
392
- }
393
- });
504
+ options.httpServer || new http_1.Server(this.createInternalRequestHandler());
394
505
  this.keepalives.subscribe('lost', this.library.remove);
395
506
  }
396
- sendErrorResponse(e, res) {
507
+ writeErrorResponse(e, res) {
397
508
  this.emit('error', e);
398
509
  this.log('Error handling request', e);
399
510
  if (e instanceof common_1.VerdantError) {
@@ -407,6 +518,30 @@ class Server extends events_1.default {
407
518
  }
408
519
  res.end();
409
520
  }
521
+ // for fetch-style
522
+ getErrorResponse(e) {
523
+ this.emit('error', e);
524
+ this.log('Error handling request', e);
525
+ if (e instanceof common_1.VerdantError) {
526
+ return new Response(JSON.stringify(e.toResponse()), {
527
+ status: e.httpStatus,
528
+ headers: {
529
+ 'Content-Type': 'application/json',
530
+ },
531
+ });
532
+ }
533
+ else {
534
+ return new Response(JSON.stringify(new common_1.VerdantError(common_1.VerdantError.Code.Unexpected).toResponse()), {
535
+ status: 500,
536
+ headers: {
537
+ 'Content-Type': 'application/json',
538
+ },
539
+ });
540
+ }
541
+ }
410
542
  }
411
543
  exports.Server = Server;
544
+ function isFetch(request) {
545
+ return 'json' in request;
546
+ }
412
547
  //# sourceMappingURL=Server.js.map