@orkestrel/middleware 0.0.19 → 0.0.20

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/README.md CHANGED
@@ -1,12 +1,19 @@
1
1
  # @orkestrel/middleware
2
2
 
3
- Batteries for the `@orkestrel/server` middleware seam — the frozen
4
- `MiddlewareHandler<TState>` / `compose` contract and its substrate ship
5
- policy-free; this package supplies the policies the server deliberately does
6
- not: boundary rendering, telemetry, compression, security headers, CORS,
7
- deadlines, trusted-proxy client facts, ETag, bearer auth, rate limiting, body
8
- parsing, sessions, CSRF, static files, and multipart uploads each a typed
9
- `options => MiddlewareHandler<TState>` factory over the shipped seam.
3
+ > Batteries for the `@orkestrel/server` middleware seam:
4
+ > `create{Noun}(options) => MiddlewareHandler<TState>` factories for error
5
+ > boundaries, telemetry, compression, security headers, CORS, deadlines,
6
+ > trusted-proxy client facts, ETag, bearer authentication, rate limiting, body
7
+ > parsing, sessions, and CSRF in the fetch-native core, plus in-memory assets,
8
+ > static files, streaming multipart uploads, and a `node:zlib` compression
9
+ > sibling in the node face.
10
+
11
+ Mount the batteries your threat model needs over the `compose` seam: each one
12
+ closes over its guarded options and returns a `MiddlewareHandler<TState>`, and
13
+ its position in the chain decides what it can see. Read
14
+ [`guides/middleware.md`](guides/middleware.md) for the ordering doctrine and
15
+ the security acceptance bar before you fix an order. Part of the `@orkestrel`
16
+ line.
10
17
 
11
18
  ## Install
12
19
 
@@ -40,10 +47,8 @@ const handle = compose<State>([boundary, security], async (_request, context) =>
40
47
  ```
41
48
 
42
49
  Each battery is a typed `options => MiddlewareHandler<TState>` factory that composes with
43
- the others through the frozen `@orkestrel/server` seam mount boundary, telemetry,
44
- compression, security headers, CORS, rate limiting, sessions (with `MemorySessionStore` or
45
- `DatabaseSessionStore` over `@orkestrel/database`), CSRF, static files, and multipart uploads
46
- in any combination, scoped with `only()` / `except()` where needed.
50
+ the others through the frozen `@orkestrel/server` seam, in any combination, scoped with
51
+ `only()` and `except()` where needed.
47
52
 
48
53
  ## Guides
49
54
 
@@ -5,12 +5,12 @@ let _orkestrel_abort = require("@orkestrel/abort");
5
5
  let _orkestrel_budget = require("@orkestrel/budget");
6
6
  let _orkestrel_timeout = require("@orkestrel/timeout");
7
7
  //#region src/core/constants.ts
8
- /** Holds the default minimum buffered body size (bytes) `createCompression` will compress. */
8
+ /** Holds `1024`, the default minimum buffered body size in bytes `createCompression` will compress. */
9
9
  var DEFAULT_COMPRESSION_THRESHOLD = 1024;
10
10
  /**
11
- * Lists the default content-codings `createCompression` offers, in preference order —
12
- * intersected at construction with what the runtime's `CompressionStream`
13
- * actually supports.
11
+ * Lists `['gzip', 'deflate']`, the default content-codings `createCompression` offers in
12
+ * preference order — intersected at construction with what the runtime's
13
+ * `CompressionStream` actually supports.
14
14
  *
15
15
  * @remarks
16
16
  * The shipped `@orkestrel/server` peer's {@link Encoding} union is
@@ -20,28 +20,29 @@ var DEFAULT_COMPRESSION_THRESHOLD = 1024;
20
20
  * admits; a node-face brotli variant, if one ships, extends this list there.
21
21
  */
22
22
  var DEFAULT_COMPRESSION_ENCODINGS = Object.freeze(["gzip", "deflate"]);
23
- /** Holds the default `X-Frame-Options` value `createSecurity` sets. */
23
+ /** Holds `'DENY'`, the default `X-Frame-Options` value `createSecurity` sets. */
24
24
  var DEFAULT_FRAME_OPTIONS = "DENY";
25
25
  /**
26
- * Holds the default `Content-Security-Policy` value `createSecurity` sets a custom
27
- * `csp` option REPLACES this wholesale, never merges.
26
+ * Holds `"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'"`,
27
+ * the default `Content-Security-Policy` value `createSecurity` sets a custom
28
+ * `csp` option replaces this wholesale, never merges.
28
29
  */
29
30
  var DEFAULT_CSP = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'";
30
- /** Holds the default `Referrer-Policy` value `createSecurity` sets. */
31
+ /** Holds `'strict-origin-when-cross-origin'`, the default `Referrer-Policy` value `createSecurity` sets. */
31
32
  var DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin";
32
- /** Holds the default `Permissions-Policy` value `createSecurity` sets. */
33
+ /** Holds `'camera=(), microphone=(), geolocation=()'`, the default `Permissions-Policy` value `createSecurity` sets. */
33
34
  var DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
34
- /** Holds the default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
35
+ /** Holds `'same-origin'`, the default `Cross-Origin-Opener-Policy` value `createSecurity` sets. */
35
36
  var DEFAULT_COOP = "same-origin";
36
- /** Holds the default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
37
+ /** Holds `'same-origin'`, the default `Cross-Origin-Resource-Policy` value `createSecurity` sets. */
37
38
  var DEFAULT_CORP = "same-origin";
38
- /** Holds the default `Origin-Agent-Cluster` value `createSecurity` sets. */
39
+ /** Holds `'?1'`, the default `Origin-Agent-Cluster` value `createSecurity` sets. */
39
40
  var DEFAULT_CLUSTER = "?1";
40
- /** Holds the value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
41
+ /** Holds `'require-corp'`, the value `createSecurity` sets for `Cross-Origin-Embedder-Policy` when `coep: true`. */
41
42
  var DEFAULT_COEP = "require-corp";
42
- /** Holds the value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
43
+ /** Holds `'max-age=31536000; includeSubDomains'`, the value `createSecurity` sets for `Strict-Transport-Security` when `hsts: true`. */
43
44
  var DEFAULT_HSTS = "max-age=31536000; includeSubDomains";
44
- /** Names the default header `createSecurity` mints/echoes a request identifier into. */
45
+ /** Names `'x-request-id'`, the default header `createSecurity` mints or echoes a request identifier into. */
45
46
  var DEFAULT_IDENTIFIER_HEADER = "x-request-id";
46
47
  /** Lists the default methods `createCors` advertises on a preflight response. */
47
48
  var DEFAULT_CORS_METHODS = Object.freeze([
@@ -54,29 +55,29 @@ var DEFAULT_CORS_METHODS = Object.freeze([
54
55
  ]);
55
56
  /** Lists the default headers `createCors` advertises on a preflight response. */
56
57
  var DEFAULT_CORS_HEADERS = Object.freeze(["Content-Type", "Authorization"]);
57
- /** Holds the default response status `createDeadline` returns when its deadline fires first. */
58
+ /** Holds `503`, the default response status `createDeadline` returns when its deadline fires first. */
58
59
  var DEFAULT_DEADLINE_STATUS = 503;
59
- /** Names the default header `createBearer` reads the token from. */
60
+ /** Names `'authorization'`, the default header `createBearer` reads the token from. */
60
61
  var DEFAULT_BEARER_HEADER = "authorization";
61
- /** Names the default scheme prefix `createBearer` strips before verification. */
62
+ /** Names `'Bearer'`, the default scheme prefix `createBearer` strips before verification. */
62
63
  var DEFAULT_BEARER_SCHEME = "Bearer";
63
- /** Holds the default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
64
+ /** Holds `10_000`, the default maximum number of distinct rate-limit keys `createLimiter` tracks before LRU eviction. */
64
65
  var DEFAULT_LIMITER_CAPACITY = 1e4;
65
- /** Holds the default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
66
+ /** Holds `10_000`, the default maximum number of distinct session ids `createMemorySessionStore` tracks before LRU (by last write) eviction. */
66
67
  var DEFAULT_SESSION_CAPACITY = 1e4;
67
- /** Holds the default 429 body message `createLimiter` sends when a key is over budget. */
68
+ /** Holds `'rate limit exceeded'`, the default 429 body message `createLimiter` sends when a key is over budget. */
68
69
  var DEFAULT_LIMITER_MESSAGE = "rate limit exceeded";
69
- /** Names the default cookie `createCookieTransport` writes the signed session id under. */
70
+ /** Names `'session'`, the default cookie `createCookieTransport` writes the signed session id under. */
70
71
  var DEFAULT_SESSION_COOKIE = "session";
71
- /** Names the default header `createHeaderTransport` carries the session id in. */
72
+ /** Names `'session-id'`, the default header `createHeaderTransport` carries the session id in. */
72
73
  var DEFAULT_SESSION_HEADER = "session-id";
73
- /** Names the default signed cookie `createCSRF` writes the CSRF token under. */
74
+ /** Names `'csrf'`, the default signed cookie `createCSRF` writes the CSRF token under. */
74
75
  var DEFAULT_CSRF_COOKIE = "csrf";
75
- /** Names the default header `createCSRF` reads a mutating request's submitted token from. */
76
+ /** Names `'x-csrf-token'`, the default header `createCSRF` reads a mutating request's submitted token from. */
76
77
  var DEFAULT_CSRF_HEADER = "x-csrf-token";
77
- /** Names the default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
78
+ /** Names `'_csrf'`, the default body field `createCSRF` falls back to reading a mutating request's submitted token from. */
78
79
  var DEFAULT_CSRF_FIELD = "_csrf";
79
- /** Lists the default methods `createCSRF` treats as safe (mint instead of verify). */
80
+ /** Lists `['GET', 'HEAD', 'OPTIONS']`, the default methods `createCSRF` treats as safe (mint instead of verify). */
80
81
  var DEFAULT_CSRF_SAFE_METHODS = Object.freeze([
81
82
  "GET",
82
83
  "HEAD",
@@ -175,7 +176,7 @@ function buildRateLimitPolicyField(max, window) {
175
176
  * never claims full CIDR generality beyond IPv4.
176
177
  *
177
178
  * @remarks
178
- * An IPv6 `trusted` roster entry is compared as an EXACT string — it must be
179
+ * An IPv6 `trusted` roster entry is compared as an exact string — it must be
179
180
  * supplied in canonical form (no zero-compression normalization, no case
180
181
  * folding) by the caller; this function performs no IPv6 normalization of
181
182
  * its own.
@@ -209,13 +210,13 @@ function matchesTrustedEntry(address, entry) {
209
210
  return (networkInt & mask) === (addressInt & mask);
210
211
  }
211
212
  /**
212
- * Walks `X-Forwarded-For` right-to-left and resolves the first UNTRUSTED hop
213
+ * Walks `X-Forwarded-For` right-to-left and resolves the first untrusted hop
213
214
  * address — `createForwarded`'s core algorithm.
214
215
  *
215
216
  * @remarks
216
217
  * Parses `X-Forwarded-For` only. With `proxies` set, trusts exactly that
217
218
  * many hops counted from the right (the closest to this server) and returns
218
- * the next one left of them; with `trusted` set, trusts every CONSECUTIVE
219
+ * the next one left of them; with `trusted` set, trusts every consecutive
219
220
  * hop from the right that matches one of the roster
220
221
  * ({@link matchesTrustedEntry}) and returns the first hop that does not. If
221
222
  * the rightmost hop (the immediate sender) does not match the roster, the
@@ -225,7 +226,7 @@ function matchesTrustedEntry(address, entry) {
225
226
  * socket peer).
226
227
  *
227
228
  * @param header - The raw `X-Forwarded-For` header value (comma-separated hops), if present
228
- * @param trust - The {@link ForwardedOptions} form in force — a trusted hop COUNT or a `trusted` CIDR/exact roster
229
+ * @param trust - The {@link ForwardedOptions} form in force — a trusted hop count or a `trusted` CIDR/exact roster
229
230
  * @returns The first untrusted hop address, or `undefined` when none qualifies
230
231
  *
231
232
  * @example
@@ -299,16 +300,17 @@ async function compressBytes(bytes, encoding) {
299
300
  return new Uint8Array(compressed);
300
301
  }
301
302
  /**
302
- * Checks whether a response is eligible for the compression/ETag buffering pipeline
303
+ * Checks whether a response must skip the compression and ETag buffering pipeline
303
304
  * — the shared cheap-skip predicate both batteries apply before ever touching
304
- * `response.arrayBuffer()`.
305
+ * `response.arrayBuffer()`, true for a `HEAD` request, a `204`/`304` or
306
+ * otherwise bodyless response, an `event-stream` response, and a response
307
+ * already carrying the header the caller is about to set.
305
308
  *
306
309
  * @remarks
307
- * Skips a `HEAD` request, a `204`/`304` or otherwise bodyless response, an
308
- * `event-stream` response (SSE buffering would hang the connection), and a
309
- * response that already carries the header the caller is about to set
310
- * (`skipHeader`, for example `Content-Encoding` for compression, `ETag` for the
311
- * ETag battery).
310
+ * Buffering an `event-stream` response would hang the connection, so SSE skips
311
+ * whatever its size. `skipHeader` is the header whose presence already answers
312
+ * the question `Content-Encoding` for compression, `ETag` for the ETag
313
+ * battery.
312
314
  *
313
315
  * @param method - The request's HTTP method
314
316
  * @param response - The candidate response
@@ -347,7 +349,7 @@ function isCompressionNegotiated(encoding) {
347
349
  }
348
350
  /**
349
351
  * Resolves an opt-in, value-bearing security header — `string | boolean`
350
- * (default OFF, `true` uses the secure default), the shape `createSecurity`'s
352
+ * (off by default, `true` uses the secure default), the shape `createSecurity`'s
351
353
  * `coep`/`hsts` options use, distinct from the plain value-or-`false` shape
352
354
  * `resolveSecurityHeader` (the peer substrate) handles.
353
355
  *
@@ -401,7 +403,7 @@ function rebuildResponse(body, response, headers) {
401
403
  * even when a later skip declines to compress) → `negotiateEncoding` over
402
404
  * `options.encodings` → {@link isCompressionNegotiated} → `isCompressibleType`
403
405
  * on `Content-Type` → a fast skip when the response already carries a
404
- * numeric `Content-Length` BELOW `options.threshold` (avoids buffering a
406
+ * numeric `Content-Length` below `options.threshold` (avoids buffering a
405
407
  * body known too small to be worth compressing) → buffer through
406
408
  * `response.arrayBuffer()` → a threshold passthrough when the buffered size
407
409
  * is still below `options.threshold` → `options.compress` → set
@@ -463,7 +465,7 @@ function transferSessionState(from, to) {
463
465
  for (const [key, value] of from.state) to.set(key, value);
464
466
  }
465
467
  /**
466
- * Determines whether a request is a CORS PREFLIGHT — an `OPTIONS` request
468
+ * Determines whether a request is a CORS preflight — an `OPTIONS` request
467
469
  * carrying an `Access-Control-Request-Method` header.
468
470
  *
469
471
  * @param method - The request's HTTP method
@@ -570,7 +572,7 @@ function validateSessionLimits(limits) {
570
572
  * @remarks
571
573
  * `state` is built on a null-prototype object (`Object.create(null)`), never
572
574
  * a `{}` literal — a session key literally named `__proto__` must round-trip
573
- * as an OWN enumerable property instead of hitting `Object.prototype`'s
575
+ * as an own enumerable property instead of hitting `Object.prototype`'s
574
576
  * `__proto__` accessor (which would silently drop the entry and risk
575
577
  * polluting the shared prototype).
576
578
  *
@@ -593,8 +595,8 @@ function snapshotSession(session) {
593
595
  * Determines whether a value implements {@link SessionInterface} — a total
594
596
  * structural guard: an `id` string, a `state` `Map`, and the `set`, `delete`,
595
597
  * and `clear` mutators. Prototype-agnostic — accepts a plain object, a
596
- * null-prototype object, AND a class instance (a real `Session`), since a
597
- * restored/stored session is routinely a class instance, not a literal.
598
+ * null-prototype object, and a class instance (a real `Session`), because a
599
+ * restored or stored session is routinely a class instance rather than a literal.
598
600
  *
599
601
  * @param value - The candidate value
600
602
  * @returns True if `value` is shaped like a {@link SessionInterface}; false otherwise
@@ -682,7 +684,7 @@ function isMultipartBody(value) {
682
684
  //#region src/core/shapers.ts
683
685
  /**
684
686
  * Holds the `@orkestrel/database` column shape for a
685
- * {@link import('./types.js').SessionRow} table pass as-is to
687
+ * {@link import('./types.js').SessionRow} table. Pass it as-is to
686
688
  * `createDatabase({ tables: { sessions: sessionColumns } })` so an app
687
689
  * declaring a durable session table never hand-writes the shape.
688
690
  *
@@ -761,7 +763,7 @@ var Session = class {
761
763
  * @remarks
762
764
  * `get` evicts a session whose idle time (`now - seen >= ttl`) or
763
765
  * absolute lifetime (`now - created >= lifetime`) has elapsed — the
764
- * lifetime check fires EVEN IF the session was continuously touched, since
766
+ * lifetime check fires even if the session was continuously touched, because
765
767
  * `created` is stamped once at the first `set` and preserved across every
766
768
  * later re-`set` of the same id. A live read touches `seen`. `delete` of
767
769
  * an absent id is a no-op.
@@ -876,7 +878,7 @@ var MemorySessionStore = class {
876
878
  * {@link MemorySessionStore}. `delete` of an absent id is a no-op (the
877
879
  * table's `remove` contract).
878
880
  *
879
- * A malformed-snapshot or failed-guard `undefined` LEAVES the row in place —
881
+ * A malformed-snapshot or failed-guard `undefined` leaves the row in place —
880
882
  * unlike the expired path, which removes it. This is deliberate: a
881
883
  * caller-contextual guard may reject a session that is still perfectly
882
884
  * valid for another flow reading the same table (a differently-shaped `S`,
@@ -946,9 +948,20 @@ var DatabaseSessionStore = class {
946
948
  * @returns A `MiddlewareHandler<TState>`
947
949
  * @throws {TypeError} When `options.expose` or `options.report` is malformed
948
950
  *
949
- * @example
951
+ * @example Mount a battery
950
952
  * ```ts
953
+ * import { createBoundary, createSecurity } from '@orkestrel/middleware'
954
+ * import type { IdentifierState } from '@orkestrel/middleware'
955
+ * import { compose } from '@orkestrel/server'
956
+ *
957
+ * interface State extends IdentifierState {}
958
+ *
951
959
  * const boundary = createBoundary({ expose: false })
960
+ * const security = createSecurity({ hsts: true })
961
+ *
962
+ * const handle = compose<State>([boundary, security], async (_request, context) => {
963
+ * return Response.json({ identifier: context.state.identifier })
964
+ * })
952
965
  * ```
953
966
  */
954
967
  function createBoundary(options) {
@@ -1037,7 +1050,8 @@ function createCompression(options) {
1037
1050
  };
1038
1051
  }
1039
1052
  /**
1040
- * Creates the security-headers + request-identifier battery.
1053
+ * Creates the security-headers + request-identifier battery — sets each documented
1054
+ * header default, and mints or echoes a request identifier.
1041
1055
  *
1042
1056
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link IdentifierState}
1043
1057
  * @param options - See {@link SecurityOptions}
@@ -1099,7 +1113,8 @@ function createSecurity(options) {
1099
1113
  };
1100
1114
  }
1101
1115
  /**
1102
- * Creates the Cross-Origin Resource Sharing battery.
1116
+ * Creates the Cross-Origin Resource Sharing battery — answers a preflight itself, and
1117
+ * reflects an allow-listed origin or serves the configured wildcard.
1103
1118
  *
1104
1119
  * @typeParam TState - The consumer's opaque per-request state type
1105
1120
  * @param options - See {@link CorsOptions}
@@ -1148,7 +1163,7 @@ function createCors(options) {
1148
1163
  * @throws {TypeError} When `options.ms` or `options.status` is malformed
1149
1164
  *
1150
1165
  * @remarks
1151
- * MUST sit OUTSIDE `createBody` in the chain — it reconstructs the inbound
1166
+ * Mount this battery outside `createBody` in the chain — it reconstructs the inbound
1152
1167
  * `Request` (to link its `signal` to the deadline `signal`), which throws if
1153
1168
  * the body was already consumed upstream (for example by `createBody`'s cached read).
1154
1169
  *
@@ -1185,7 +1200,8 @@ function createDeadline(options) {
1185
1200
  };
1186
1201
  }
1187
1202
  /**
1188
- * Creates the trusted-proxy client-IP resolver battery.
1203
+ * Creates the trusted-proxy client-IP resolver battery — walks `X-Forwarded-For` past
1204
+ * the hops its options declare trusted.
1189
1205
  *
1190
1206
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link ClientState} and {@link ConnectionState}
1191
1207
  * @param options - See {@link ForwardedOptions}
@@ -1216,7 +1232,7 @@ function createForwarded(options) {
1216
1232
  };
1217
1233
  }
1218
1234
  /**
1219
- * Creates the dynamic response `ETag` + conditional GET battery.
1235
+ * Creates the dynamic response `ETag` + conditional GET battery (RFC 7232).
1220
1236
  *
1221
1237
  * @typeParam TState - The consumer's opaque per-request state type
1222
1238
  * @param options - See {@link ETagOptions}
@@ -1254,7 +1270,8 @@ function createETag(options) {
1254
1270
  };
1255
1271
  }
1256
1272
  /**
1257
- * Creates the bearer-token authentication battery.
1273
+ * Creates the bearer-token authentication battery — reads the token from its header and
1274
+ * verifies it with `verifyToken`.
1258
1275
  *
1259
1276
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}
1260
1277
  * @param options - See {@link BearerOptions}
@@ -1290,7 +1307,8 @@ function createBearer(options) {
1290
1307
  };
1291
1308
  }
1292
1309
  /**
1293
- * Creates the fixed-window rate-limiting battery.
1310
+ * Creates the fixed-window rate-limiting battery — checks a key's budget before
1311
+ * consuming it, so one window admits exactly `max` requests.
1294
1312
  *
1295
1313
  * @typeParam TState - The consumer's opaque per-request state type, must carry {@link BearerState}, {@link ClientState}, and {@link ConnectionState}
1296
1314
  * @param options - See {@link LimiterOptions}
@@ -1379,7 +1397,7 @@ function createLimiter(options) {
1379
1397
  * The shipped `MiddlewareContext.body()` is a parameterless, server-owned
1380
1398
  * cache (`ServerOptions.limit` governs its size cap) — this battery carries
1381
1399
  * no `limit`/`decompression` options. `state.body` is
1382
- * stashed from the SAME awaited call the 400 check reads — `context.body()`
1400
+ * stashed from the same awaited call the 400 check reads — `context.body()`
1383
1401
  * is never invoked twice.
1384
1402
  *
1385
1403
  * @example
@@ -1538,8 +1556,8 @@ function createCSRF(options) {
1538
1556
  };
1539
1557
  }
1540
1558
  /**
1541
- * Scopes a battery to run ONLY on a set of exact pathnames — elsewhere it
1542
- * steps aside through `next()`.
1559
+ * Scopes a battery to a set of exact pathnames and nowhere else outside that set
1560
+ * it steps aside through `next()`.
1543
1561
  *
1544
1562
  * @typeParam TState - The consumer's opaque per-request state type
1545
1563
  * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`
@@ -1559,8 +1577,8 @@ function only(paths, handler) {
1559
1577
  };
1560
1578
  }
1561
1579
  /**
1562
- * Scopes a battery to run everywhere EXCEPT a set of exact pathnamesthere
1563
- * it steps aside through `next()`.
1580
+ * Scopes a battery to every pathname outside a set of exact oneson that set it
1581
+ * steps aside through `next()`.
1564
1582
  *
1565
1583
  * @typeParam TState - The consumer's opaque per-request state type
1566
1584
  * @param paths - One pathname, or a set of pathnames, matched exactly against `context.url.pathname`