@fluojs/http 1.1.2 → 3.0.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.
- package/README.ko.md +430 -15
- package/README.md +437 -15
- package/dist/access-log-observer.d.ts +77 -0
- package/dist/access-log-observer.d.ts.map +1 -0
- package/dist/access-log-observer.js +171 -0
- package/dist/adapter.d.ts +9 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +15 -1
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +18 -16
- package/dist/adapters/dto-binding-plan.d.ts +2 -2
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
- package/dist/adapters/dto-binding-plan.js +13 -4
- package/dist/adapters/dto-validation-adapter.d.ts +2 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +2 -2
- package/dist/byte-range-response.d.ts +71 -0
- package/dist/byte-range-response.d.ts.map +1 -0
- package/dist/byte-range-response.js +347 -0
- package/dist/client-identity.d.ts +8 -4
- package/dist/client-identity.d.ts.map +1 -1
- package/dist/client-identity.js +12 -93
- package/dist/compiled-route-identity.d.ts +17 -0
- package/dist/compiled-route-identity.d.ts.map +1 -0
- package/dist/compiled-route-identity.js +29 -0
- package/dist/connection.d.ts +67 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +480 -0
- package/dist/context/request-context-node-store.d.ts +6 -0
- package/dist/context/request-context-node-store.d.ts.map +1 -1
- package/dist/context/request-context-node-store.js +14 -0
- package/dist/context/request-context.d.ts +5 -5
- package/dist/context/request-context.d.ts.map +1 -1
- package/dist/context/request-context.js +8 -83
- package/dist/context/request-id.d.ts +10 -0
- package/dist/context/request-id.d.ts.map +1 -0
- package/dist/context/request-id.js +27 -0
- package/dist/context/sse.d.ts +13 -0
- package/dist/context/sse.d.ts.map +1 -1
- package/dist/context/sse.js +36 -6
- package/dist/cookie-helpers.d.ts +47 -0
- package/dist/cookie-helpers.d.ts.map +1 -0
- package/dist/cookie-helpers.js +166 -0
- package/dist/decorators.d.ts +52 -19
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +57 -10
- package/dist/dispatch/conditional-request-policy.d.ts +67 -0
- package/dist/dispatch/conditional-request-policy.d.ts.map +1 -0
- package/dist/dispatch/conditional-request-policy.js +297 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts +8 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
- package/dist/dispatch/dispatch-content-negotiation.js +89 -42
- package/dist/dispatch/dispatch-error-negotiation.d.ts +26 -0
- package/dist/dispatch/dispatch-error-negotiation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-negotiation.js +110 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +1 -10
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-error-policy.js +1 -33
- package/dist/dispatch/dispatch-error-representation.d.ts +18 -0
- package/dist/dispatch/dispatch-error-representation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-representation.js +125 -0
- package/dist/dispatch/dispatch-response-metadata.d.ts +10 -0
- package/dist/dispatch/dispatch-response-metadata.d.ts.map +1 -0
- package/dist/dispatch/dispatch-response-metadata.js +12 -0
- package/dist/dispatch/dispatch-response-policy.d.ts +33 -5
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +149 -18
- package/dist/dispatch/dispatcher.d.ts +5 -1
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +178 -107
- package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -1
- package/dist/dispatch/fast-path/debug-visibility.js +6 -3
- package/dist/dispatch/fast-path/dispatcher-state.d.ts +10 -0
- package/dist/dispatch/fast-path/dispatcher-state.d.ts.map +1 -0
- package/dist/dispatch/fast-path/dispatcher-state.js +52 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility-checker.js +27 -15
- package/dist/dispatch/fast-path/eligibility.d.ts +16 -10
- package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility.js +10 -4
- package/dist/dispatch/fast-path/fast-path-executor.d.ts +14 -1
- package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -1
- package/dist/dispatch/fast-path/fast-path-executor.js +23 -6
- package/dist/dispatch/request-abort.d.ts +22 -0
- package/dist/dispatch/request-abort.d.ts.map +1 -0
- package/dist/dispatch/request-abort.js +30 -0
- package/dist/dispatch/response-integration.d.ts +55 -0
- package/dist/dispatch/response-integration.d.ts.map +1 -0
- package/dist/dispatch/response-integration.js +57 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +19 -0
- package/dist/guards.d.ts +1 -1
- package/dist/guards.d.ts.map +1 -1
- package/dist/header-helpers.d.ts +59 -0
- package/dist/header-helpers.d.ts.map +1 -0
- package/dist/header-helpers.js +185 -0
- package/dist/index.d.ts +1 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -15
- package/dist/index.portable.d.ts +25 -0
- package/dist/index.portable.d.ts.map +1 -0
- package/dist/index.portable.js +21 -0
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +100 -15
- package/dist/middleware/correlation.d.ts +9 -1
- package/dist/middleware/correlation.d.ts.map +1 -1
- package/dist/middleware/correlation.js +25 -14
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +7 -8
- package/dist/middleware/middleware.d.ts +3 -3
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts +13 -2
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +1 -0
- package/dist/static-assets.d.ts +77 -0
- package/dist/static-assets.d.ts.map +1 -0
- package/dist/static-assets.js +270 -0
- package/dist/types.d.ts +225 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +71 -1
- package/package.json +19 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,cAAc,CAAC;AAGvD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,cAAc,CAAC;AAGvD,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,IAAI,EAEL,MAAM,aAAa,CAAC;AAMrB;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,cAAc,GAAG,sBAAsB,GAC7C,KAAK,IAAI,qBAAqB,GAAG,uBAAuB,CAE1D;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAUxE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EACzD,eAAe,EAAE,CAAC,EAClB,GAAG,MAAM,EAAE,MAAM,EAAE,GAClB,qBAAqB,CAEvB;AA8CD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,SAAS,CAAC,cAAc,GAAG,sBAAsB,CAAC,EAAE,EACjE,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,IAAI,GACb,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MiddlewareContext, Middleware } from '../types.js';
|
|
2
|
+
import type { TrustProxyPolicy } from '../connection.js';
|
|
2
3
|
/** Snapshot of one key's current rate-limit window state. */
|
|
3
4
|
export interface RateLimitStoreEntry {
|
|
4
5
|
count: number;
|
|
@@ -18,10 +19,20 @@ export interface RateLimitOptions {
|
|
|
18
19
|
keyResolver?: (ctx: MiddlewareContext) => string;
|
|
19
20
|
store?: RateLimitStore;
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Broad legacy compatibility mode for forwarding headers.
|
|
23
|
+
*
|
|
24
|
+
* This trusts the complete `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`
|
|
25
|
+
* chain and is not limited to the direct peer. Use `trustProxy` instead for
|
|
26
|
+
* new deployments with a known proxy boundary.
|
|
23
27
|
*/
|
|
24
28
|
trustProxyHeaders?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Explicit proxy boundary used for default client identity resolution.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Prefer this policy over the broad legacy `trustProxyHeaders` mode.
|
|
34
|
+
*/
|
|
35
|
+
trustProxy?: TrustProxyPolicy;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Create the built-in in-memory store for request rate-limit windows.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,6DAA6D;AAC7D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC7F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,CAAC;IACjD,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AASD;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,cAAc,CAwC3D;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAkD/E"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { MaybePromise } from '@fluojs/core';
|
|
2
|
+
import { type ByteRangeResponseSource } from './byte-range-response.js';
|
|
3
|
+
import type { Middleware, ResponseValidators } from './types.js';
|
|
4
|
+
/** Content encodings that a static asset source may select. */
|
|
5
|
+
export type StaticAssetContentEncoding = 'br' | 'gzip';
|
|
6
|
+
/** Request encoding names considered while selecting a static representation. */
|
|
7
|
+
export type StaticAssetAcceptedEncoding = StaticAssetContentEncoding | 'identity';
|
|
8
|
+
/** Metadata for one selected static representation. */
|
|
9
|
+
export interface StaticAsset {
|
|
10
|
+
/** Media type for the logical asset path. */
|
|
11
|
+
readonly contentType: string;
|
|
12
|
+
/** Optional representation encoding selected by the asset source. */
|
|
13
|
+
readonly contentEncoding?: StaticAssetContentEncoding;
|
|
14
|
+
/** Portable bytes or lazy stream factory for the selected representation. */
|
|
15
|
+
readonly source: ByteRangeResponseSource;
|
|
16
|
+
/** Exact selected representation length in bytes. */
|
|
17
|
+
readonly size: number;
|
|
18
|
+
/** Validators emitted and evaluated for the selected representation. */
|
|
19
|
+
readonly validators?: ResponseValidators;
|
|
20
|
+
/** Whether selection can vary by `Accept-Encoding`, including an identity fallback. */
|
|
21
|
+
readonly variesByEncoding?: boolean;
|
|
22
|
+
/** Releases source-owned resources after the response settles. */
|
|
23
|
+
readonly dispose?: () => MaybePromise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Context supplied to an application-owned static asset source. */
|
|
26
|
+
export interface StaticAssetResolveContext {
|
|
27
|
+
/** Ordered encodings accepted by the request. */
|
|
28
|
+
readonly acceptedEncodings: readonly StaticAssetAcceptedEncoding[];
|
|
29
|
+
/** Request cancellation signal when the adapter exposes one. */
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
/** Resolution outcome for an existing asset without an acceptable representation. */
|
|
33
|
+
export interface StaticAssetNotAcceptable {
|
|
34
|
+
/** Signals that the requested asset exists but no representation is acceptable. */
|
|
35
|
+
readonly notAcceptable: true;
|
|
36
|
+
}
|
|
37
|
+
/** Result returned by an application-owned static asset source. */
|
|
38
|
+
export type StaticAssetResolution = StaticAsset | StaticAssetNotAcceptable | undefined;
|
|
39
|
+
/** Runtime-neutral source for static asset representations. */
|
|
40
|
+
export interface StaticAssetSource {
|
|
41
|
+
/**
|
|
42
|
+
* Resolves a normalized relative asset path.
|
|
43
|
+
*
|
|
44
|
+
* Paths never begin with `/`; every `/`-separated segment is non-empty, is not
|
|
45
|
+
* `.` or `..`, and contains no backslash or NUL.
|
|
46
|
+
*
|
|
47
|
+
* @param path Normalized relative asset path.
|
|
48
|
+
* @param context Request capabilities relevant to source selection.
|
|
49
|
+
* @returns The selected representation, an explicit unacceptable outcome, or `undefined` when no asset exists.
|
|
50
|
+
*/
|
|
51
|
+
resolve(path: string, context: StaticAssetResolveContext): MaybePromise<StaticAssetResolution>;
|
|
52
|
+
}
|
|
53
|
+
/** Static asset middleware configuration. */
|
|
54
|
+
export interface StaticAssetsMiddlewareOptions {
|
|
55
|
+
/** Required application-owned source; the portable layer never opens a filesystem itself. */
|
|
56
|
+
readonly source: StaticAssetSource;
|
|
57
|
+
/** URL prefix owned by this middleware. Defaults to `/`. */
|
|
58
|
+
readonly prefix?: string;
|
|
59
|
+
/** Dotfile policy. `deny` writes 403; `ignore` leaves the request to later middleware/routes. */
|
|
60
|
+
readonly dotfiles?: 'allow' | 'deny' | 'ignore';
|
|
61
|
+
/** Directory indexes resolved only for paths ending in `/`. Defaults to `false`. */
|
|
62
|
+
readonly index?: false | string | readonly string[];
|
|
63
|
+
/** Cache-Control value emitted for served and conditional responses. Defaults to `public, max-age=0`. */
|
|
64
|
+
readonly cacheControl?: false | string;
|
|
65
|
+
}
|
|
66
|
+
/** Static middleware that serves a required explicit {@link StaticAssetSource}. */
|
|
67
|
+
export interface StaticAssetsMiddleware extends Middleware {
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Creates portable static asset middleware without assuming filesystem access.
|
|
71
|
+
*
|
|
72
|
+
* @param options Source and URL/security policy for one static mount.
|
|
73
|
+
* @returns Middleware that serves matching `GET` and `HEAD` asset requests.
|
|
74
|
+
* @throws {TypeError} When configuration is structurally invalid.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createStaticAssetsMiddleware(options: StaticAssetsMiddlewareOptions): StaticAssetsMiddleware;
|
|
77
|
+
//# sourceMappingURL=static-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-assets.d.ts","sourceRoot":"","sources":["../src/static-assets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,EACV,UAAU,EAEV,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAKpB,+DAA+D;AAC/D,MAAM,MAAM,0BAA0B,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvD,iFAAiF;AACjF,MAAM,MAAM,2BAA2B,GAAG,0BAA0B,GAAG,UAAU,CAAC;AAElF,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,eAAe,CAAC,EAAE,0BAA0B,CAAC;IACtD,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC,uFAAuF;IACvF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,oEAAoE;AACpE,MAAM,WAAW,yBAAyB;IACxC,iDAAiD;IACjD,QAAQ,CAAC,iBAAiB,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACnE,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,qFAAqF;AACrF,MAAM,WAAW,wBAAwB;IACvC,mFAAmF;IACnF,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC;CAC9B;AAED,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,wBAAwB,GAAG,SAAS,CAAC;AAEvF,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAChG;AAED,6CAA6C;AAC7C,MAAM,WAAW,6BAA6B;IAC5C,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,oFAAoF;IACpF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACpD,yGAAyG;IACzG,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACxC;AAED,mFAAmF;AACnF,MAAM,WAAW,sBAAuB,SAAQ,UAAU;CAAG;AAE7D;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,sBAAsB,CAyGxB"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { writeByteRangeResponse } from './byte-range-response.js';
|
|
2
|
+
import { appendVaryHeader, readFirstNonEmptyRequestHeaderValue } from './header-helpers.js';
|
|
3
|
+
import { applyResponseValidators, resolveConditionalRequestRepresentation, writeConditionalResponse } from './dispatch/conditional-request-policy.js';
|
|
4
|
+
const defaultCacheControl = 'public, max-age=0';
|
|
5
|
+
const defaultDotfiles = 'ignore';
|
|
6
|
+
|
|
7
|
+
/** Content encodings that a static asset source may select. */
|
|
8
|
+
|
|
9
|
+
/** Request encoding names considered while selecting a static representation. */
|
|
10
|
+
|
|
11
|
+
/** Metadata for one selected static representation. */
|
|
12
|
+
|
|
13
|
+
/** Context supplied to an application-owned static asset source. */
|
|
14
|
+
|
|
15
|
+
/** Resolution outcome for an existing asset without an acceptable representation. */
|
|
16
|
+
|
|
17
|
+
/** Result returned by an application-owned static asset source. */
|
|
18
|
+
|
|
19
|
+
/** Runtime-neutral source for static asset representations. */
|
|
20
|
+
|
|
21
|
+
/** Static asset middleware configuration. */
|
|
22
|
+
|
|
23
|
+
/** Static middleware that serves a required explicit {@link StaticAssetSource}. */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates portable static asset middleware without assuming filesystem access.
|
|
27
|
+
*
|
|
28
|
+
* @param options Source and URL/security policy for one static mount.
|
|
29
|
+
* @returns Middleware that serves matching `GET` and `HEAD` asset requests.
|
|
30
|
+
* @throws {TypeError} When configuration is structurally invalid.
|
|
31
|
+
*/
|
|
32
|
+
export function createStaticAssetsMiddleware(options) {
|
|
33
|
+
const configuration = resolveStaticAssetsConfiguration(options);
|
|
34
|
+
return {
|
|
35
|
+
async handle(context, next) {
|
|
36
|
+
const assetPath = resolveStaticAssetPath(context.request.path, configuration.prefix, configuration.dotfiles);
|
|
37
|
+
if (!isStaticAssetMethod(context.request.method)) {
|
|
38
|
+
await next();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (assetPath === 'denied') {
|
|
42
|
+
context.response.setStatus(403);
|
|
43
|
+
await context.response.send(undefined);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!assetPath) {
|
|
47
|
+
await next();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const sourceContext = createStaticAssetResolveContext(context);
|
|
51
|
+
const resolution = await resolveStaticAsset(configuration, assetPath, sourceContext);
|
|
52
|
+
if (resolution === 'denied') {
|
|
53
|
+
context.response.setStatus(403);
|
|
54
|
+
await context.response.send(undefined);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!resolution) {
|
|
58
|
+
await next();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (isStaticAssetNotAcceptable(resolution)) {
|
|
62
|
+
appendVaryHeader(context.response, 'Accept-Encoding');
|
|
63
|
+
context.response.setHeader('Content-Length', '0');
|
|
64
|
+
context.response.setStatus(406);
|
|
65
|
+
await context.response.send(undefined, {
|
|
66
|
+
compression: false
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (!isStaticAssetEncodingAccepted(resolution, sourceContext.acceptedEncodings)) {
|
|
72
|
+
appendVaryHeader(context.response, 'Accept-Encoding');
|
|
73
|
+
context.response.setHeader('Content-Length', '0');
|
|
74
|
+
context.response.setStatus(406);
|
|
75
|
+
await context.response.send(undefined, {
|
|
76
|
+
compression: false
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (configuration.cacheControl !== false) {
|
|
81
|
+
context.response.setHeader('Cache-Control', configuration.cacheControl);
|
|
82
|
+
}
|
|
83
|
+
if (resolution.contentEncoding) {
|
|
84
|
+
context.response.setHeader('Content-Encoding', resolution.contentEncoding);
|
|
85
|
+
}
|
|
86
|
+
if (resolution.contentEncoding || resolution.variesByEncoding) {
|
|
87
|
+
appendVaryHeader(context.response, 'Accept-Encoding');
|
|
88
|
+
}
|
|
89
|
+
const conditional = resolveConditionalRequestRepresentation(context.request, {
|
|
90
|
+
exists: true,
|
|
91
|
+
validators: resolution.validators
|
|
92
|
+
});
|
|
93
|
+
if (conditional.outcome !== 'proceed') {
|
|
94
|
+
await writeConditionalResponse(context.response, conditional.outcome, conditional.validators, {
|
|
95
|
+
compression: false
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
context.response.setStatus(200);
|
|
100
|
+
await writeByteRangeResponse({
|
|
101
|
+
applySuccessResponseMetadata: () => {
|
|
102
|
+
applyResponseValidators(context.response, conditional.validators);
|
|
103
|
+
},
|
|
104
|
+
compression: false,
|
|
105
|
+
entry: {
|
|
106
|
+
contentType: resolution.contentType,
|
|
107
|
+
size: resolution.size,
|
|
108
|
+
source: resolution.source
|
|
109
|
+
},
|
|
110
|
+
request: context.request,
|
|
111
|
+
response: context.response,
|
|
112
|
+
validators: conditional.validators
|
|
113
|
+
});
|
|
114
|
+
} finally {
|
|
115
|
+
await resolution.dispose?.();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function resolveStaticAssetsConfiguration(options) {
|
|
121
|
+
if (!options || typeof options !== 'object' || !options.source || typeof options.source.resolve !== 'function') {
|
|
122
|
+
throw new TypeError('Static asset middleware requires an explicit asset source.');
|
|
123
|
+
}
|
|
124
|
+
const dotfiles = options.dotfiles ?? defaultDotfiles;
|
|
125
|
+
if (dotfiles !== 'allow' && dotfiles !== 'deny' && dotfiles !== 'ignore') {
|
|
126
|
+
throw new TypeError('Static asset dotfiles policy must be "allow", "deny", or "ignore".');
|
|
127
|
+
}
|
|
128
|
+
const cacheControl = options.cacheControl ?? defaultCacheControl;
|
|
129
|
+
if (cacheControl !== false && (typeof cacheControl !== 'string' || cacheControl.trim() === '')) {
|
|
130
|
+
throw new TypeError('Static asset cacheControl must be a non-empty string or false.');
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
cacheControl,
|
|
134
|
+
dotfiles,
|
|
135
|
+
index: normalizeIndex(options.index),
|
|
136
|
+
prefix: normalizePrefix(options.prefix),
|
|
137
|
+
source: options.source
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function normalizePrefix(prefix) {
|
|
141
|
+
const value = prefix ?? '/';
|
|
142
|
+
if (typeof value !== 'string' || !value.startsWith('/')) {
|
|
143
|
+
throw new TypeError('Static asset prefix must begin with "/".');
|
|
144
|
+
}
|
|
145
|
+
const normalized = value.replace(/\/+$/, '');
|
|
146
|
+
return normalized === '' ? '/' : normalized;
|
|
147
|
+
}
|
|
148
|
+
function normalizeIndex(index) {
|
|
149
|
+
if (index === undefined || index === false) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const values = typeof index === 'string' ? [index] : index;
|
|
153
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
154
|
+
throw new TypeError('Static asset index must be false, a non-empty filename, or a non-empty filename array.');
|
|
155
|
+
}
|
|
156
|
+
return values.map(value => {
|
|
157
|
+
if (typeof value !== 'string' || value === '' || value.includes('/') || value.includes('\\') || value === '.' || value === '..') {
|
|
158
|
+
throw new TypeError('Static asset index names must be plain filenames.');
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function resolveStaticAssetPath(requestPath, prefix, dotfiles) {
|
|
164
|
+
if (!matchesStaticPrefix(requestPath, prefix)) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const encodedPath = prefix === '/' ? requestPath : requestPath.slice(prefix.length);
|
|
168
|
+
const directory = encodedPath.endsWith('/');
|
|
169
|
+
const encodedSegments = encodedPath.split('/').filter(segment => segment.length > 0);
|
|
170
|
+
const segments = [];
|
|
171
|
+
for (const encodedSegment of encodedSegments) {
|
|
172
|
+
let segment;
|
|
173
|
+
try {
|
|
174
|
+
segment = decodeURIComponent(encodedSegment);
|
|
175
|
+
} catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (segment === '' || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\') || segment.includes('\0')) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if (segment.startsWith('.') && dotfiles !== 'allow') {
|
|
182
|
+
return dotfiles === 'deny' ? 'denied' : undefined;
|
|
183
|
+
}
|
|
184
|
+
segments.push(segment);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
directory,
|
|
188
|
+
path: segments.join('/')
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function matchesStaticPrefix(requestPath, prefix) {
|
|
192
|
+
return prefix === '/' ? requestPath.startsWith('/') : requestPath === prefix || requestPath.startsWith(`${prefix}/`);
|
|
193
|
+
}
|
|
194
|
+
function isStaticAssetMethod(method) {
|
|
195
|
+
const normalized = method.toUpperCase();
|
|
196
|
+
return normalized === 'GET' || normalized === 'HEAD';
|
|
197
|
+
}
|
|
198
|
+
function createStaticAssetResolveContext(context) {
|
|
199
|
+
return {
|
|
200
|
+
acceptedEncodings: parseAcceptedEncodings(context),
|
|
201
|
+
signal: context.request.signal
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function parseAcceptedEncodings(context) {
|
|
205
|
+
const value = readFirstNonEmptyRequestHeaderValue(context.request, 'accept-encoding');
|
|
206
|
+
if (!value) {
|
|
207
|
+
return ['identity'];
|
|
208
|
+
}
|
|
209
|
+
const qualities = new Map();
|
|
210
|
+
for (const candidate of value.split(',')) {
|
|
211
|
+
const [name, ...parameters] = candidate.trim().toLowerCase().split(';');
|
|
212
|
+
const quality = parameters.find(parameter => parameter.trim().startsWith('q='));
|
|
213
|
+
const parsedQuality = parseAcceptEncodingQuality(quality?.trim().slice(2));
|
|
214
|
+
if (!name || parsedQuality === undefined) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
qualities.set(name, parsedQuality);
|
|
218
|
+
}
|
|
219
|
+
return ['br', 'gzip', 'identity'].map(encoding => ({
|
|
220
|
+
encoding,
|
|
221
|
+
quality: resolveAcceptEncodingQuality(encoding, qualities)
|
|
222
|
+
})).filter(candidate => candidate.quality > 0).sort((left, right) => right.quality - left.quality || encodingPriority(left.encoding) - encodingPriority(right.encoding)).map(candidate => candidate.encoding);
|
|
223
|
+
}
|
|
224
|
+
function parseAcceptEncodingQuality(value) {
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
if (!/^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(value)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return Number(value);
|
|
232
|
+
}
|
|
233
|
+
function resolveAcceptEncodingQuality(encoding, qualities) {
|
|
234
|
+
const explicit = qualities.get(encoding);
|
|
235
|
+
if (explicit !== undefined) {
|
|
236
|
+
return explicit;
|
|
237
|
+
}
|
|
238
|
+
const wildcard = qualities.get('*');
|
|
239
|
+
return encoding === 'identity' ? wildcard === 0 ? 0 : 1 : wildcard ?? 0;
|
|
240
|
+
}
|
|
241
|
+
function encodingPriority(encoding) {
|
|
242
|
+
return encoding === 'br' ? 0 : encoding === 'gzip' ? 1 : 2;
|
|
243
|
+
}
|
|
244
|
+
async function resolveStaticAsset(configuration, assetPath, context) {
|
|
245
|
+
if (assetPath.directory) {
|
|
246
|
+
for (const indexName of configuration.index) {
|
|
247
|
+
if (indexName.startsWith('.') && configuration.dotfiles !== 'allow') {
|
|
248
|
+
if (configuration.dotfiles === 'deny') {
|
|
249
|
+
return 'denied';
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const asset = await configuration.source.resolve(assetPath.path === '' ? indexName : `${assetPath.path}/${indexName}`, context);
|
|
254
|
+
if (asset) {
|
|
255
|
+
return asset;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
if (assetPath.path === '') {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return await configuration.source.resolve(assetPath.path, context);
|
|
264
|
+
}
|
|
265
|
+
function isStaticAssetNotAcceptable(resolution) {
|
|
266
|
+
return resolution !== undefined && 'notAcceptable' in resolution && resolution.notAcceptable;
|
|
267
|
+
}
|
|
268
|
+
function isStaticAssetEncodingAccepted(asset, acceptedEncodings) {
|
|
269
|
+
return acceptedEncodings.includes(asset.contentEncoding ?? 'identity');
|
|
270
|
+
}
|