@orkestrel/test 0.0.8 → 0.0.10
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/src/browser/index.d.ts +452 -6
- package/dist/src/browser/index.js +489 -24
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +288 -8
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +180 -5
- package/dist/src/core/index.d.ts +180 -5
- package/dist/src/core/index.js +281 -9
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +244 -1
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +143 -2
- package/dist/src/server/index.d.ts +143 -2
- package/dist/src/server/index.js +240 -3
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -215,6 +215,39 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
|
215
215
|
*/
|
|
216
216
|
export declare function removeTree(path: string): void;
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Drives a real client upgrade request against a loopback port and reports what the server did.
|
|
220
|
+
*
|
|
221
|
+
* @param port - The port the server listens on at `127.0.0.1`.
|
|
222
|
+
* @param options - Optional request path, offered subprotocols, time bounds, and abort signal.
|
|
223
|
+
* @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it
|
|
224
|
+
* selected, or a refusal with the status it answered.
|
|
225
|
+
* @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the
|
|
226
|
+
* abort reason, or an `Error` when a bound is invalid or the server does not answer within the
|
|
227
|
+
* budget.
|
|
228
|
+
* @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and
|
|
229
|
+
* `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.
|
|
230
|
+
* The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise
|
|
231
|
+
* settles on whichever arrives first, so a second event changes nothing. The client socket is
|
|
232
|
+
* destroyed before every settlement, on the claimed path because an upgraded socket is detached from
|
|
233
|
+
* the request and outlives it otherwise. The request is made with no agent, so no pooled connection
|
|
234
|
+
* survives the call to keep a suite's event loop alive.
|
|
235
|
+
*
|
|
236
|
+
* A server that accepts the connection and answers nothing raises no transport error, so the budget
|
|
237
|
+
* is what ends that call: the rejection names the port and path it was waiting on. The interval is
|
|
238
|
+
* validated for consistency with the wait family but is not used, because this helper parks on the
|
|
239
|
+
* request's events.
|
|
240
|
+
*
|
|
241
|
+
* A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the
|
|
242
|
+
* refused arm's member, and a claimed upgrade produced no plain answer.
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })
|
|
246
|
+
* // { claimed: true, protocol: 'chat' }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export declare function requestUpgrade(port: number, options?: UpgradeOptions): Promise<UpgradeResult>;
|
|
250
|
+
|
|
218
251
|
/**
|
|
219
252
|
* Resolves a target that stays below a root directory.
|
|
220
253
|
*
|
|
@@ -243,10 +276,11 @@ export declare interface ScratchInterface {
|
|
|
243
276
|
*
|
|
244
277
|
* @param target - A relative or absolute file path contained by the scratch directory.
|
|
245
278
|
* @param text - The file contents.
|
|
279
|
+
* @returns The absolute path of the written file.
|
|
246
280
|
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
247
281
|
* link, or a file, or the host refuses to write the file.
|
|
248
282
|
*/
|
|
249
|
-
write(target: string, text: string):
|
|
283
|
+
write(target: string, text: string): string;
|
|
250
284
|
/**
|
|
251
285
|
* Reads a file.
|
|
252
286
|
*
|
|
@@ -294,12 +328,13 @@ export declare interface ScratchInterface {
|
|
|
294
328
|
* @param source - The destination path the link points at. The stored value is a path naming that
|
|
295
329
|
* destination, but its exact text is not promised. The path may name a destination outside the
|
|
296
330
|
* scratch directory and is not containment-checked.
|
|
331
|
+
* @returns The absolute path of the created link, whatever host mechanism created it.
|
|
297
332
|
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
298
333
|
* link, or a file, or the host refuses to create the link, including a host that creates no
|
|
299
334
|
* symbolic link when the source names an existing non-directory.
|
|
300
335
|
* @remarks {@link createLink} owns the host-specific link mechanism.
|
|
301
336
|
*/
|
|
302
|
-
link(target: string, source: string):
|
|
337
|
+
link(target: string, source: string): string;
|
|
303
338
|
/**
|
|
304
339
|
* Removes a file, an empty directory, or a directory and its descendants.
|
|
305
340
|
*
|
|
@@ -339,6 +374,112 @@ export declare interface ScratchOptions {
|
|
|
339
374
|
readonly files?: Readonly<Record<string, string>>;
|
|
340
375
|
}
|
|
341
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.
|
|
379
|
+
*
|
|
380
|
+
* @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including
|
|
381
|
+
* every host refusal.
|
|
382
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
383
|
+
* remove it afterwards, both propagate.
|
|
384
|
+
* @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
|
|
385
|
+
* rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
|
|
386
|
+
* as a `Buffer` because the byte survives no string round trip.
|
|
387
|
+
*/
|
|
388
|
+
export declare function supportsBytes(): boolean;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks whether this host treats two names differing only by case as distinct files.
|
|
392
|
+
*
|
|
393
|
+
* @returns True if `A` and `a` hold the contents each was written with; false otherwise, including
|
|
394
|
+
* every host refusal.
|
|
395
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
396
|
+
* remove it afterwards, both propagate.
|
|
397
|
+
* @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading
|
|
398
|
+
* an answer about case folding rather than an answer about two unrelated files. A case-folding volume
|
|
399
|
+
* routes the second write onto the first entry, so reading the first back returns the second's
|
|
400
|
+
* contents and the answer is false. The answer is true on a typical POSIX host and false on a
|
|
401
|
+
* case-folding Windows or macOS volume.
|
|
402
|
+
*/
|
|
403
|
+
export declare function supportsCase(): boolean;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Checks whether this host links a directory, by creating one link and reading through it.
|
|
407
|
+
*
|
|
408
|
+
* @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches
|
|
409
|
+
* the destination's contents; false otherwise, including every host refusal.
|
|
410
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
411
|
+
* remove it afterwards, both propagate.
|
|
412
|
+
* @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which
|
|
413
|
+
* needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.
|
|
414
|
+
* The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
|
|
415
|
+
* probes and cleans up after itself, so a host whose answer changes is read again rather than
|
|
416
|
+
* remembered.
|
|
417
|
+
*/
|
|
418
|
+
export declare function supportsDirectoryLinks(): boolean;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Checks whether this host links a file, by creating one link and reading the file through it.
|
|
422
|
+
*
|
|
423
|
+
* @returns True if the file's contents are readable through the link; false otherwise, including
|
|
424
|
+
* every host refusal.
|
|
425
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
426
|
+
* remove it afterwards, both propagate.
|
|
427
|
+
* @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows
|
|
428
|
+
* grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the
|
|
429
|
+
* answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a
|
|
430
|
+
* file through a link and a proof that reads one back cannot run. This is a separate question from
|
|
431
|
+
* {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction
|
|
432
|
+
* while answering this one false.
|
|
433
|
+
*/
|
|
434
|
+
export declare function supportsFileLinks(): boolean;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.
|
|
438
|
+
*
|
|
439
|
+
* @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,
|
|
440
|
+
* including every host refusal.
|
|
441
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
442
|
+
* remove it afterwards, both propagate.
|
|
443
|
+
* @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the
|
|
444
|
+
* answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing
|
|
445
|
+
* it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
|
|
446
|
+
* bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
|
|
447
|
+
* rather than reading this.
|
|
448
|
+
*/
|
|
449
|
+
export declare function supportsMode(): boolean;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Options for driving a client upgrade request.
|
|
453
|
+
*
|
|
454
|
+
* @remarks The time bounds and abort signal bound the wait for the server's answer, so a server
|
|
455
|
+
* that accepts the connection and never answers ends the call rather than parking it.
|
|
456
|
+
*/
|
|
457
|
+
export declare interface UpgradeOptions extends WaitOptions {
|
|
458
|
+
/** The request path, written with its leading slash. Defaults to `/`. */
|
|
459
|
+
readonly path?: string;
|
|
460
|
+
/**
|
|
461
|
+
* The subprotocol tokens the request offers. They are sent as one comma-separated
|
|
462
|
+
* `Sec-WebSocket-Protocol` field, and an empty or omitted list sends no field at all.
|
|
463
|
+
*/
|
|
464
|
+
readonly protocols?: readonly string[];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* What one server did with a client upgrade request.
|
|
469
|
+
*
|
|
470
|
+
* @remarks `claimed` is the discriminant. The claimed arm carries `protocol`, the subprotocol the
|
|
471
|
+
* server selected, which is `undefined` when it selected none; a claimed upgrade produced no plain
|
|
472
|
+
* answer, so it carries no status and the `101` on the wire is deliberately not reported as one.
|
|
473
|
+
* The refused arm carries `status`, the plain answer's status, and no subprotocol.
|
|
474
|
+
*/
|
|
475
|
+
export declare type UpgradeResult = {
|
|
476
|
+
readonly claimed: true;
|
|
477
|
+
readonly protocol: string | undefined;
|
|
478
|
+
} | {
|
|
479
|
+
readonly claimed: false;
|
|
480
|
+
readonly status: number;
|
|
481
|
+
};
|
|
482
|
+
|
|
342
483
|
/**
|
|
343
484
|
* Waits for a socket to close, accepting a peer reset as a forced close.
|
|
344
485
|
*
|
|
@@ -215,6 +215,39 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
|
215
215
|
*/
|
|
216
216
|
export declare function removeTree(path: string): void;
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Drives a real client upgrade request against a loopback port and reports what the server did.
|
|
220
|
+
*
|
|
221
|
+
* @param port - The port the server listens on at `127.0.0.1`.
|
|
222
|
+
* @param options - Optional request path, offered subprotocols, time bounds, and abort signal.
|
|
223
|
+
* @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it
|
|
224
|
+
* selected, or a refusal with the status it answered.
|
|
225
|
+
* @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the
|
|
226
|
+
* abort reason, or an `Error` when a bound is invalid or the server does not answer within the
|
|
227
|
+
* budget.
|
|
228
|
+
* @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and
|
|
229
|
+
* `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.
|
|
230
|
+
* The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise
|
|
231
|
+
* settles on whichever arrives first, so a second event changes nothing. The client socket is
|
|
232
|
+
* destroyed before every settlement, on the claimed path because an upgraded socket is detached from
|
|
233
|
+
* the request and outlives it otherwise. The request is made with no agent, so no pooled connection
|
|
234
|
+
* survives the call to keep a suite's event loop alive.
|
|
235
|
+
*
|
|
236
|
+
* A server that accepts the connection and answers nothing raises no transport error, so the budget
|
|
237
|
+
* is what ends that call: the rejection names the port and path it was waiting on. The interval is
|
|
238
|
+
* validated for consistency with the wait family but is not used, because this helper parks on the
|
|
239
|
+
* request's events.
|
|
240
|
+
*
|
|
241
|
+
* A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the
|
|
242
|
+
* refused arm's member, and a claimed upgrade produced no plain answer.
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })
|
|
246
|
+
* // { claimed: true, protocol: 'chat' }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export declare function requestUpgrade(port: number, options?: UpgradeOptions): Promise<UpgradeResult>;
|
|
250
|
+
|
|
218
251
|
/**
|
|
219
252
|
* Resolves a target that stays below a root directory.
|
|
220
253
|
*
|
|
@@ -243,10 +276,11 @@ export declare interface ScratchInterface {
|
|
|
243
276
|
*
|
|
244
277
|
* @param target - A relative or absolute file path contained by the scratch directory.
|
|
245
278
|
* @param text - The file contents.
|
|
279
|
+
* @returns The absolute path of the written file.
|
|
246
280
|
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
247
281
|
* link, or a file, or the host refuses to write the file.
|
|
248
282
|
*/
|
|
249
|
-
write(target: string, text: string):
|
|
283
|
+
write(target: string, text: string): string;
|
|
250
284
|
/**
|
|
251
285
|
* Reads a file.
|
|
252
286
|
*
|
|
@@ -294,12 +328,13 @@ export declare interface ScratchInterface {
|
|
|
294
328
|
* @param source - The destination path the link points at. The stored value is a path naming that
|
|
295
329
|
* destination, but its exact text is not promised. The path may name a destination outside the
|
|
296
330
|
* scratch directory and is not containment-checked.
|
|
331
|
+
* @returns The absolute path of the created link, whatever host mechanism created it.
|
|
297
332
|
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
298
333
|
* link, or a file, or the host refuses to create the link, including a host that creates no
|
|
299
334
|
* symbolic link when the source names an existing non-directory.
|
|
300
335
|
* @remarks {@link createLink} owns the host-specific link mechanism.
|
|
301
336
|
*/
|
|
302
|
-
link(target: string, source: string):
|
|
337
|
+
link(target: string, source: string): string;
|
|
303
338
|
/**
|
|
304
339
|
* Removes a file, an empty directory, or a directory and its descendants.
|
|
305
340
|
*
|
|
@@ -339,6 +374,112 @@ export declare interface ScratchOptions {
|
|
|
339
374
|
readonly files?: Readonly<Record<string, string>>;
|
|
340
375
|
}
|
|
341
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.
|
|
379
|
+
*
|
|
380
|
+
* @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including
|
|
381
|
+
* every host refusal.
|
|
382
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
383
|
+
* remove it afterwards, both propagate.
|
|
384
|
+
* @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
|
|
385
|
+
* rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
|
|
386
|
+
* as a `Buffer` because the byte survives no string round trip.
|
|
387
|
+
*/
|
|
388
|
+
export declare function supportsBytes(): boolean;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks whether this host treats two names differing only by case as distinct files.
|
|
392
|
+
*
|
|
393
|
+
* @returns True if `A` and `a` hold the contents each was written with; false otherwise, including
|
|
394
|
+
* every host refusal.
|
|
395
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
396
|
+
* remove it afterwards, both propagate.
|
|
397
|
+
* @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading
|
|
398
|
+
* an answer about case folding rather than an answer about two unrelated files. A case-folding volume
|
|
399
|
+
* routes the second write onto the first entry, so reading the first back returns the second's
|
|
400
|
+
* contents and the answer is false. The answer is true on a typical POSIX host and false on a
|
|
401
|
+
* case-folding Windows or macOS volume.
|
|
402
|
+
*/
|
|
403
|
+
export declare function supportsCase(): boolean;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Checks whether this host links a directory, by creating one link and reading through it.
|
|
407
|
+
*
|
|
408
|
+
* @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches
|
|
409
|
+
* the destination's contents; false otherwise, including every host refusal.
|
|
410
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
411
|
+
* remove it afterwards, both propagate.
|
|
412
|
+
* @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which
|
|
413
|
+
* needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.
|
|
414
|
+
* The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
|
|
415
|
+
* probes and cleans up after itself, so a host whose answer changes is read again rather than
|
|
416
|
+
* remembered.
|
|
417
|
+
*/
|
|
418
|
+
export declare function supportsDirectoryLinks(): boolean;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Checks whether this host links a file, by creating one link and reading the file through it.
|
|
422
|
+
*
|
|
423
|
+
* @returns True if the file's contents are readable through the link; false otherwise, including
|
|
424
|
+
* every host refusal.
|
|
425
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
426
|
+
* remove it afterwards, both propagate.
|
|
427
|
+
* @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows
|
|
428
|
+
* grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the
|
|
429
|
+
* answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a
|
|
430
|
+
* file through a link and a proof that reads one back cannot run. This is a separate question from
|
|
431
|
+
* {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction
|
|
432
|
+
* while answering this one false.
|
|
433
|
+
*/
|
|
434
|
+
export declare function supportsFileLinks(): boolean;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.
|
|
438
|
+
*
|
|
439
|
+
* @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,
|
|
440
|
+
* including every host refusal.
|
|
441
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
442
|
+
* remove it afterwards, both propagate.
|
|
443
|
+
* @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the
|
|
444
|
+
* answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing
|
|
445
|
+
* it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
|
|
446
|
+
* bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
|
|
447
|
+
* rather than reading this.
|
|
448
|
+
*/
|
|
449
|
+
export declare function supportsMode(): boolean;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Options for driving a client upgrade request.
|
|
453
|
+
*
|
|
454
|
+
* @remarks The time bounds and abort signal bound the wait for the server's answer, so a server
|
|
455
|
+
* that accepts the connection and never answers ends the call rather than parking it.
|
|
456
|
+
*/
|
|
457
|
+
export declare interface UpgradeOptions extends WaitOptions {
|
|
458
|
+
/** The request path, written with its leading slash. Defaults to `/`. */
|
|
459
|
+
readonly path?: string;
|
|
460
|
+
/**
|
|
461
|
+
* The subprotocol tokens the request offers. They are sent as one comma-separated
|
|
462
|
+
* `Sec-WebSocket-Protocol` field, and an empty or omitted list sends no field at all.
|
|
463
|
+
*/
|
|
464
|
+
readonly protocols?: readonly string[];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* What one server did with a client upgrade request.
|
|
469
|
+
*
|
|
470
|
+
* @remarks `claimed` is the discriminant. The claimed arm carries `protocol`, the subprotocol the
|
|
471
|
+
* server selected, which is `undefined` when it selected none; a claimed upgrade produced no plain
|
|
472
|
+
* answer, so it carries no status and the `101` on the wire is deliberately not reported as one.
|
|
473
|
+
* The refused arm carries `status`, the plain answer's status, and no subprotocol.
|
|
474
|
+
*/
|
|
475
|
+
export declare type UpgradeResult = {
|
|
476
|
+
readonly claimed: true;
|
|
477
|
+
readonly protocol: string | undefined;
|
|
478
|
+
} | {
|
|
479
|
+
readonly claimed: false;
|
|
480
|
+
readonly status: number;
|
|
481
|
+
};
|
|
482
|
+
|
|
342
483
|
/**
|
|
343
484
|
* Waits for a socket to close, accepting a peer reset as a forced close.
|
|
344
485
|
*
|
package/dist/src/server/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
+
import { request } from "node:http";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { waitForDelay } from "../core/index.js";
|
|
5
8
|
import { once } from "node:events";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
9
|
//#region src/server/constants.ts
|
|
8
10
|
/**
|
|
9
11
|
* The attempts `removeTree` makes before rethrowing a retryable removal error.
|
|
@@ -312,6 +314,239 @@ async function destroyScratch(scratch, options) {
|
|
|
312
314
|
await waitForDelay(interval);
|
|
313
315
|
}
|
|
314
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Drives a real client upgrade request against a loopback port and reports what the server did.
|
|
319
|
+
*
|
|
320
|
+
* @param port - The port the server listens on at `127.0.0.1`.
|
|
321
|
+
* @param options - Optional request path, offered subprotocols, time bounds, and abort signal.
|
|
322
|
+
* @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it
|
|
323
|
+
* selected, or a refusal with the status it answered.
|
|
324
|
+
* @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the
|
|
325
|
+
* abort reason, or an `Error` when a bound is invalid or the server does not answer within the
|
|
326
|
+
* budget.
|
|
327
|
+
* @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and
|
|
328
|
+
* `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.
|
|
329
|
+
* The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise
|
|
330
|
+
* settles on whichever arrives first, so a second event changes nothing. The client socket is
|
|
331
|
+
* destroyed before every settlement, on the claimed path because an upgraded socket is detached from
|
|
332
|
+
* the request and outlives it otherwise. The request is made with no agent, so no pooled connection
|
|
333
|
+
* survives the call to keep a suite's event loop alive.
|
|
334
|
+
*
|
|
335
|
+
* A server that accepts the connection and answers nothing raises no transport error, so the budget
|
|
336
|
+
* is what ends that call: the rejection names the port and path it was waiting on. The interval is
|
|
337
|
+
* validated for consistency with the wait family but is not used, because this helper parks on the
|
|
338
|
+
* request's events.
|
|
339
|
+
*
|
|
340
|
+
* A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the
|
|
341
|
+
* refused arm's member, and a claimed upgrade produced no plain answer.
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })
|
|
345
|
+
* // { claimed: true, protocol: 'chat' }
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
async function requestUpgrade(port, options) {
|
|
349
|
+
const budget = options?.budget ?? 1e3;
|
|
350
|
+
const interval = options?.interval ?? 10;
|
|
351
|
+
if (!Number.isFinite(budget) || budget < 0) throw new Error("Upgrade budget must be finite and non-negative");
|
|
352
|
+
if (!Number.isFinite(interval) || interval < 0) throw new Error("Upgrade interval must be finite and non-negative");
|
|
353
|
+
const signal = options?.signal;
|
|
354
|
+
signal?.throwIfAborted();
|
|
355
|
+
const path = options?.path ?? "/";
|
|
356
|
+
const target = `127.0.0.1:${port}${path}`;
|
|
357
|
+
const headers = {
|
|
358
|
+
connection: "Upgrade",
|
|
359
|
+
upgrade: "websocket"
|
|
360
|
+
};
|
|
361
|
+
const protocols = options?.protocols ?? [];
|
|
362
|
+
if (protocols.length > 0) headers["sec-websocket-protocol"] = protocols.join(", ");
|
|
363
|
+
const request$1 = request({
|
|
364
|
+
agent: false,
|
|
365
|
+
headers,
|
|
366
|
+
host: "127.0.0.1",
|
|
367
|
+
path,
|
|
368
|
+
port
|
|
369
|
+
});
|
|
370
|
+
const settled = Promise.withResolvers();
|
|
371
|
+
const expiry = Promise.withResolvers();
|
|
372
|
+
const aborted = Promise.withResolvers();
|
|
373
|
+
const subscription = new AbortController();
|
|
374
|
+
request$1.on("upgrade", (response, socket) => {
|
|
375
|
+
const protocol = response.headers["sec-websocket-protocol"];
|
|
376
|
+
socket.destroy();
|
|
377
|
+
settled.resolve({
|
|
378
|
+
claimed: true,
|
|
379
|
+
protocol
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
request$1.on("response", (response) => {
|
|
383
|
+
const status = response.statusCode;
|
|
384
|
+
response.destroy();
|
|
385
|
+
request$1.destroy();
|
|
386
|
+
if (status === void 0) {
|
|
387
|
+
settled.reject(/* @__PURE__ */ new Error(`Upgrade request to ${target} was answered without a status`));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
settled.resolve({
|
|
391
|
+
claimed: false,
|
|
392
|
+
status
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
request$1.on("error", (error) => {
|
|
396
|
+
request$1.destroy();
|
|
397
|
+
settled.reject(error);
|
|
398
|
+
});
|
|
399
|
+
signal?.addEventListener("abort", () => aborted.reject(signal.reason), {
|
|
400
|
+
once: true,
|
|
401
|
+
signal: subscription.signal
|
|
402
|
+
});
|
|
403
|
+
const timer = setTimeout(() => {
|
|
404
|
+
expiry.reject(/* @__PURE__ */ new Error(`Upgrade request to ${target} was not answered within ${budget}ms`));
|
|
405
|
+
}, budget);
|
|
406
|
+
request$1.end();
|
|
407
|
+
try {
|
|
408
|
+
return await Promise.race([
|
|
409
|
+
settled.promise,
|
|
410
|
+
expiry.promise,
|
|
411
|
+
aborted.promise
|
|
412
|
+
]);
|
|
413
|
+
} finally {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
subscription.abort();
|
|
416
|
+
request$1.destroy();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Checks whether this host links a directory, by creating one link and reading through it.
|
|
421
|
+
*
|
|
422
|
+
* @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches
|
|
423
|
+
* the destination's contents; false otherwise, including every host refusal.
|
|
424
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
425
|
+
* remove it afterwards, both propagate.
|
|
426
|
+
* @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which
|
|
427
|
+
* needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.
|
|
428
|
+
* The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
|
|
429
|
+
* probes and cleans up after itself, so a host whose answer changes is read again rather than
|
|
430
|
+
* remembered.
|
|
431
|
+
*/
|
|
432
|
+
function supportsDirectoryLinks() {
|
|
433
|
+
const directory = mkdtempSync(join(tmpdir(), "orkestrel-test-directory-links-"));
|
|
434
|
+
try {
|
|
435
|
+
const source = join(directory, "source");
|
|
436
|
+
const link = join(directory, "link");
|
|
437
|
+
mkdirSync(source);
|
|
438
|
+
writeFileSync(join(source, "marker.txt"), "marked");
|
|
439
|
+
symlinkSync(source, link, "junction");
|
|
440
|
+
return lstatSync(link).isSymbolicLink() && statSync(link).isDirectory() && readFileSync(join(link, "marker.txt"), "utf8") === "marked";
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
} finally {
|
|
444
|
+
removeTree(directory);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Checks whether this host links a file, by creating one link and reading the file through it.
|
|
449
|
+
*
|
|
450
|
+
* @returns True if the file's contents are readable through the link; false otherwise, including
|
|
451
|
+
* every host refusal.
|
|
452
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
453
|
+
* remove it afterwards, both propagate.
|
|
454
|
+
* @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows
|
|
455
|
+
* grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the
|
|
456
|
+
* answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a
|
|
457
|
+
* file through a link and a proof that reads one back cannot run. This is a separate question from
|
|
458
|
+
* {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction
|
|
459
|
+
* while answering this one false.
|
|
460
|
+
*/
|
|
461
|
+
function supportsFileLinks() {
|
|
462
|
+
const directory = mkdtempSync(join(tmpdir(), "orkestrel-test-file-links-"));
|
|
463
|
+
try {
|
|
464
|
+
const source = join(directory, "source.txt");
|
|
465
|
+
const link = join(directory, "link.txt");
|
|
466
|
+
writeFileSync(source, "linked");
|
|
467
|
+
symlinkSync(source, link, "file");
|
|
468
|
+
return readFileSync(link, "utf8") === "linked";
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
} finally {
|
|
472
|
+
removeTree(directory);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.
|
|
477
|
+
*
|
|
478
|
+
* @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,
|
|
479
|
+
* including every host refusal.
|
|
480
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
481
|
+
* remove it afterwards, both propagate.
|
|
482
|
+
* @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the
|
|
483
|
+
* answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing
|
|
484
|
+
* it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
|
|
485
|
+
* bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
|
|
486
|
+
* rather than reading this.
|
|
487
|
+
*/
|
|
488
|
+
function supportsMode() {
|
|
489
|
+
const directory = mkdtempSync(join(tmpdir(), "orkestrel-test-mode-"));
|
|
490
|
+
try {
|
|
491
|
+
const path = join(directory, "moded");
|
|
492
|
+
mkdirSync(path, { mode: 448 });
|
|
493
|
+
return (statSync(path).mode & 511) === 448;
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
} finally {
|
|
497
|
+
removeTree(directory);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Checks whether this host treats two names differing only by case as distinct files.
|
|
502
|
+
*
|
|
503
|
+
* @returns True if `A` and `a` hold the contents each was written with; false otherwise, including
|
|
504
|
+
* every host refusal.
|
|
505
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
506
|
+
* remove it afterwards, both propagate.
|
|
507
|
+
* @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading
|
|
508
|
+
* an answer about case folding rather than an answer about two unrelated files. A case-folding volume
|
|
509
|
+
* routes the second write onto the first entry, so reading the first back returns the second's
|
|
510
|
+
* contents and the answer is false. The answer is true on a typical POSIX host and false on a
|
|
511
|
+
* case-folding Windows or macOS volume.
|
|
512
|
+
*/
|
|
513
|
+
function supportsCase() {
|
|
514
|
+
const directory = mkdtempSync(join(tmpdir(), "orkestrel-test-case-"));
|
|
515
|
+
try {
|
|
516
|
+
const upper = join(directory, "A");
|
|
517
|
+
const lower = join(directory, "a");
|
|
518
|
+
writeFileSync(upper, "upper");
|
|
519
|
+
writeFileSync(lower, "lower");
|
|
520
|
+
return readFileSync(upper, "utf8") === "upper" && readFileSync(lower, "utf8") === "lower";
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
} finally {
|
|
524
|
+
removeTree(directory);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.
|
|
529
|
+
*
|
|
530
|
+
* @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including
|
|
531
|
+
* every host refusal.
|
|
532
|
+
* @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
|
|
533
|
+
* remove it afterwards, both propagate.
|
|
534
|
+
* @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
|
|
535
|
+
* rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
|
|
536
|
+
* as a `Buffer` because the byte survives no string round trip.
|
|
537
|
+
*/
|
|
538
|
+
function supportsBytes() {
|
|
539
|
+
const directory = mkdtempSync(join(tmpdir(), "orkestrel-test-bytes-"));
|
|
540
|
+
try {
|
|
541
|
+
const name = Buffer.concat([Buffer.from(`${directory}${sep}`), Buffer.from([128])]);
|
|
542
|
+
writeFileSync(name, "raw");
|
|
543
|
+
return readFileSync(name, "utf8") === "raw";
|
|
544
|
+
} catch {
|
|
545
|
+
return false;
|
|
546
|
+
} finally {
|
|
547
|
+
removeTree(directory);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
315
550
|
//#endregion
|
|
316
551
|
//#region src/server/factories.ts
|
|
317
552
|
/**
|
|
@@ -360,6 +595,7 @@ function createScratch(options) {
|
|
|
360
595
|
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
361
596
|
mkdirSync(dirname(candidate), { recursive: true });
|
|
362
597
|
writeFileSync(candidate, text);
|
|
598
|
+
return candidate;
|
|
363
599
|
},
|
|
364
600
|
read(target) {
|
|
365
601
|
const candidate = resolveContained(path, target);
|
|
@@ -403,6 +639,7 @@ function createScratch(options) {
|
|
|
403
639
|
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
404
640
|
mkdirSync(dirname(candidate), { recursive: true });
|
|
405
641
|
createLink(candidate, source);
|
|
642
|
+
return candidate;
|
|
406
643
|
},
|
|
407
644
|
remove(target) {
|
|
408
645
|
const candidate = resolveContained(path, target);
|
|
@@ -495,6 +732,6 @@ function createCookieJar() {
|
|
|
495
732
|
};
|
|
496
733
|
}
|
|
497
734
|
//#endregion
|
|
498
|
-
export { REMOVE_TREE_MAX_ATTEMPTS, REMOVE_TREE_RETRYABLE_CODES, REMOVE_TREE_RETRY_DELAY_MS, createCookieJar, createLink, createLoopback, createScratch, destroyScratch, isExcluded, isRunning, matchesIdentity, readInventory, removeTree, resolveContained, waitForSocketClose };
|
|
735
|
+
export { REMOVE_TREE_MAX_ATTEMPTS, REMOVE_TREE_RETRYABLE_CODES, REMOVE_TREE_RETRY_DELAY_MS, createCookieJar, createLink, createLoopback, createScratch, destroyScratch, isExcluded, isRunning, matchesIdentity, readInventory, removeTree, requestUpgrade, resolveContained, supportsBytes, supportsCase, supportsDirectoryLinks, supportsFileLinks, supportsMode, waitForSocketClose };
|
|
499
736
|
|
|
500
737
|
//# sourceMappingURL=index.js.map
|