@pristine-ts/http 4.0.2 → 4.0.3

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.
Files changed (38) hide show
  1. package/dist/lib/cjs/cors/cors-preflight-response.interface.js +3 -0
  2. package/dist/lib/cjs/cors/cors-preflight-response.interface.js.map +1 -0
  3. package/dist/lib/cjs/cors/cors-request-context.interface.js +3 -0
  4. package/dist/lib/cjs/cors/cors-request-context.interface.js.map +1 -0
  5. package/dist/lib/cjs/cors/cors-request.handler.js +207 -0
  6. package/dist/lib/cjs/cors/cors-request.handler.js.map +1 -0
  7. package/dist/lib/cjs/cors/cors.js +20 -0
  8. package/dist/lib/cjs/cors/cors.js.map +1 -0
  9. package/dist/lib/cjs/http.configuration-keys.js +14 -0
  10. package/dist/lib/cjs/http.configuration-keys.js.map +1 -1
  11. package/dist/lib/cjs/http.module.js +101 -0
  12. package/dist/lib/cjs/http.module.js.map +1 -1
  13. package/dist/lib/cjs/servers/kernel.http-server.js +54 -5
  14. package/dist/lib/cjs/servers/kernel.http-server.js.map +1 -1
  15. package/dist/lib/esm/cors/cors-preflight-response.interface.js +2 -0
  16. package/dist/lib/esm/cors/cors-preflight-response.interface.js.map +1 -0
  17. package/dist/lib/esm/cors/cors-request-context.interface.js +2 -0
  18. package/dist/lib/esm/cors/cors-request-context.interface.js.map +1 -0
  19. package/dist/lib/esm/cors/cors-request.handler.js +204 -0
  20. package/dist/lib/esm/cors/cors-request.handler.js.map +1 -0
  21. package/dist/lib/esm/cors/cors.js +4 -0
  22. package/dist/lib/esm/cors/cors.js.map +1 -0
  23. package/dist/lib/esm/http.configuration-keys.js +14 -0
  24. package/dist/lib/esm/http.configuration-keys.js.map +1 -1
  25. package/dist/lib/esm/http.module.js +101 -0
  26. package/dist/lib/esm/http.module.js.map +1 -1
  27. package/dist/lib/esm/servers/kernel.http-server.js +54 -5
  28. package/dist/lib/esm/servers/kernel.http-server.js.map +1 -1
  29. package/dist/lib/tsconfig.cjs.tsbuildinfo +1 -1
  30. package/dist/lib/tsconfig.tsbuildinfo +1 -1
  31. package/dist/types/cors/cors-preflight-response.interface.d.ts +20 -0
  32. package/dist/types/cors/cors-request-context.interface.d.ts +36 -0
  33. package/dist/types/cors/cors-request.handler.d.ts +94 -0
  34. package/dist/types/cors/cors.d.ts +3 -0
  35. package/dist/types/http.configuration-keys.d.ts +22 -0
  36. package/dist/types/http.module.d.ts +1 -0
  37. package/dist/types/servers/kernel.http-server.d.ts +13 -1
  38. package/package.json +10 -10
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The answer `CorsRequestHandler` produces for a CORS preflight that must be short-circuited
3
+ * before routing. `KernelHttpServer` writes `statusCode` and `headers` straight to the
4
+ * `ServerResponse` without ever invoking `kernel.handle()` — a preflight never corresponds to a
5
+ * real route.
6
+ */
7
+ export interface CorsPreflightResponseInterface {
8
+ /**
9
+ * The status code to write. Always `204 No Content` for a preflight.
10
+ */
11
+ statusCode: number;
12
+ /**
13
+ * The response headers to write. Populated with the `Access-Control-*` set when the origin is
14
+ * allow-listed; an empty object when it is not (the browser then blocks for lack of an
15
+ * `Access-Control-Allow-Origin`, and we never reflect an arbitrary origin or emit `"*"`).
16
+ */
17
+ headers: {
18
+ [name: string]: string;
19
+ };
20
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The normalized, transport-agnostic view of a request that `CorsRequestHandler` needs to make
3
+ * a CORS decision. Extracted from Node's `IncomingMessage` (via `CorsRequestHandler.buildContext`)
4
+ * so the decision logic can be unit-tested without a live socket or a Node request object.
5
+ */
6
+ export interface CorsRequestContextInterface {
7
+ /**
8
+ * The upper-cased HTTP method (e.g. `"GET"`, `"OPTIONS"`).
9
+ */
10
+ method: string;
11
+ /**
12
+ * The `Origin` request header, if present. Absent for same-origin and non-browser callers.
13
+ */
14
+ origin?: string;
15
+ /**
16
+ * The `Host` request header, if present. Compared against the `allowed-hosts` allowlist as a
17
+ * DNS-rebinding defense for loopback servers.
18
+ */
19
+ host?: string;
20
+ /**
21
+ * The `Access-Control-Request-Method` preflight header. Its presence on an `OPTIONS` request
22
+ * is what distinguishes a CORS preflight from an ordinary `OPTIONS`.
23
+ */
24
+ accessControlRequestMethod?: string;
25
+ /**
26
+ * The `Access-Control-Request-Headers` preflight header, listing the headers the actual
27
+ * request intends to send.
28
+ */
29
+ accessControlRequestHeaders?: string;
30
+ /**
31
+ * `true` when the request carried `Access-Control-Request-Private-Network: true` — the Chrome
32
+ * Private Network Access preflight signal emitted when a public/secure context targets a more
33
+ * private address space (e.g. a loopback daemon).
34
+ */
35
+ accessControlRequestPrivateNetwork: boolean;
36
+ }
@@ -0,0 +1,94 @@
1
+ import { CorsRequestContextInterface } from "./cors-request-context.interface";
2
+ import { CorsPreflightResponseInterface } from "./cors-preflight-response.interface";
3
+ /**
4
+ * The config-driven CORS policy engine that `KernelHttpServer` consults inside its request path,
5
+ * BEFORE `kernel.handle()` — the one place that sees the raw request/response and can answer a
6
+ * preflight or reject an origin without the networking `Router` ever running.
7
+ *
8
+ * All decision methods are pure functions of the injected configuration plus a normalized
9
+ * {@link CorsRequestContextInterface}, so the full behavior matrix is unit-testable by
10
+ * constructing this class directly (no live socket, no Node request object required).
11
+ *
12
+ * ## Activation
13
+ * CORS is **inactive unless configured**. Two independent switches:
14
+ * - `cors.allowed-origins` non-empty → origin/preflight/header-echo logic activates.
15
+ * - `cors.allowed-hosts` non-empty → the Host-header allowlist (DNS-rebinding defense) activates.
16
+ * With neither set, every method is a no-op and the server behaves exactly as it did before CORS
17
+ * existed.
18
+ *
19
+ * ## Guarantees
20
+ * - Never emits `Access-Control-Allow-Origin: *` — only an exact echo of an allow-listed Origin.
21
+ * - Never reflects an arbitrary Origin — an Origin absent from the allowlist gets no ACAO headers.
22
+ */
23
+ export declare class CorsRequestHandler {
24
+ private readonly allowedOrigins;
25
+ private readonly allowedMethods;
26
+ private readonly allowedHeaders;
27
+ private readonly exposedHeaders;
28
+ private readonly maxAge;
29
+ private readonly allowCredentials;
30
+ private readonly allowPrivateNetwork;
31
+ private readonly allowedHosts;
32
+ constructor(allowedOrigins: string | string[], allowedMethods: string | string[], allowedHeaders: string | string[], exposedHeaders: string | string[], maxAge: number, allowCredentials: boolean, allowPrivateNetwork: boolean, allowedHosts: string | string[]);
33
+ /**
34
+ * `true` when an origin allowlist is configured — gates the preflight short-circuit and the
35
+ * `Access-Control-Allow-Origin` echo on actual responses.
36
+ */
37
+ get isOriginCheckEnabled(): boolean;
38
+ /**
39
+ * `true` when a Host allowlist is configured — gates the DNS-rebinding 403.
40
+ */
41
+ get isHostCheckEnabled(): boolean;
42
+ /**
43
+ * Whether any CORS feature is active. `KernelHttpServer` can skip all CORS work when this is
44
+ * `false`, keeping the un-configured request path byte-for-byte identical to before.
45
+ */
46
+ get isEnabled(): boolean;
47
+ /**
48
+ * Normalizes a raw Node request into the transport-agnostic {@link CorsRequestContextInterface}.
49
+ * Node lower-cases header names on `IncomingMessage`, and multi-valued headers arrive as arrays;
50
+ * we take the method + the CORS-relevant headers and coerce them to the single-string shape the
51
+ * decision methods expect.
52
+ */
53
+ buildContext(method: string, headers: {
54
+ [key: string]: string | string[] | undefined;
55
+ }): CorsRequestContextInterface;
56
+ /**
57
+ * Host-header allowlist check (loopback DNS-rebinding defense). Returns `true` (allow) when no
58
+ * allowlist is configured; otherwise the request's `Host` must exactly match a configured entry
59
+ * (case-insensitive, since host names are). A missing `Host` against a configured allowlist is
60
+ * rejected. `KernelHttpServer` turns a `false` here into a `403` before any routing.
61
+ */
62
+ isHostAllowed(host: string | undefined): boolean;
63
+ /**
64
+ * Exact-match origin allowlist check. An absent Origin is never "allowed" (there is nothing to
65
+ * echo). We never wildcard and never reflect an arbitrary Origin.
66
+ */
67
+ isOriginAllowed(origin: string | undefined): boolean;
68
+ /**
69
+ * Decides whether `context` is a CORS preflight that must be answered before routing, and if so
70
+ * returns the `204` response (status + headers) to write. Returns `undefined` when the request is
71
+ * not a preflight we own — the caller then lets it route normally.
72
+ *
73
+ * A CORS preflight is an `OPTIONS` carrying `Access-Control-Request-Method`. We only take over
74
+ * preflight handling when an origin allowlist is configured (same-origin requests never send
75
+ * `Access-Control-Request-Method`, so app-owned `OPTIONS` routes are never stolen). An allow-listed
76
+ * Origin gets the full `Access-Control-*` set; a disallowed Origin still gets a bare `204` with no
77
+ * ACAO — the browser blocks it, and we never leak an arbitrary Origin.
78
+ */
79
+ buildPreflightResponse(context: CorsRequestContextInterface): CorsPreflightResponseInterface | undefined;
80
+ /**
81
+ * The CORS headers to add to an ACTUAL (non-preflight) response — success OR error. Returns an
82
+ * empty object when origin-checking is off or the Origin is not allow-listed. Applied to 4xx/5xx
83
+ * responses too: without `Access-Control-Allow-Origin` the browser cannot read an error body.
84
+ */
85
+ buildResponseHeaders(origin: string | undefined): {
86
+ [name: string]: string;
87
+ };
88
+ /**
89
+ * Coerces a list-shaped config value into a trimmed, empty-stripped `string[]`. Accepts both the
90
+ * comma-separated string form (env vars / defaults) and the `string[]` form (programmatic
91
+ * `kernel.start()` config).
92
+ */
93
+ private normalizeList;
94
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./cors-preflight-response.interface";
2
+ export * from "./cors-request-context.interface";
3
+ export * from "./cors-request.handler";
@@ -17,6 +17,20 @@ export declare const HttpConfigurationKeys: {
17
17
  readonly KernelServerPort: "pristine.http.kernel-server.port";
18
18
  readonly KernelServerTlsKeyPath: "pristine.http.kernel-server.tls.key-path";
19
19
  readonly KernelServerTlsCertPath: "pristine.http.kernel-server.tls.cert-path";
20
+ /**
21
+ * CORS keys consumed by `CorsRequestHandler` inside `KernelHttpServer`'s request path.
22
+ * CORS is inactive unless `cors.allowed-origins` (and/or `cors.allowed-hosts`) is set —
23
+ * a server with none of these configured behaves exactly as it did before CORS existed.
24
+ * Each list key accepts a comma-separated string or a `string[]`.
25
+ */
26
+ readonly CorsAllowedOrigins: "pristine.http.cors.allowed-origins";
27
+ readonly CorsAllowedMethods: "pristine.http.cors.allowed-methods";
28
+ readonly CorsAllowedHeaders: "pristine.http.cors.allowed-headers";
29
+ readonly CorsExposedHeaders: "pristine.http.cors.exposed-headers";
30
+ readonly CorsMaxAge: "pristine.http.cors.max-age";
31
+ readonly CorsAllowCredentials: "pristine.http.cors.allow-credentials";
32
+ readonly CorsAllowPrivateNetwork: "pristine.http.cors.allow-private-network";
33
+ readonly CorsAllowedHosts: "pristine.http.cors.allowed-hosts";
20
34
  };
21
35
  /**
22
36
  * The expected runtime types for each configuration value defined by `@pristine-ts/http`.
@@ -30,6 +44,14 @@ export interface HttpConfigurationValueMap {
30
44
  "pristine.http.kernel-server.port": number;
31
45
  "pristine.http.kernel-server.tls.key-path": string;
32
46
  "pristine.http.kernel-server.tls.cert-path": string;
47
+ "pristine.http.cors.allowed-origins": string;
48
+ "pristine.http.cors.allowed-methods": string;
49
+ "pristine.http.cors.allowed-headers": string;
50
+ "pristine.http.cors.exposed-headers": string;
51
+ "pristine.http.cors.max-age": number;
52
+ "pristine.http.cors.allow-credentials": boolean;
53
+ "pristine.http.cors.allow-private-network": boolean;
54
+ "pristine.http.cors.allowed-hosts": string;
33
55
  }
34
56
  /**
35
57
  * Augments the global `PristineConfigurationValueMap` (defined in `@pristine-ts/common`)
@@ -2,6 +2,7 @@ import { ModuleInterface } from "@pristine-ts/common";
2
2
  export * from "./http.module.keyname";
3
3
  export * from "./commands/commands";
4
4
  export * from "./clients/clients";
5
+ export * from "./cors/cors";
5
6
  export * from "./enums/enums";
6
7
  export * from "./errors/errors";
7
8
  export * from "./interceptors/interceptors";
@@ -1,5 +1,6 @@
1
1
  import { EventIdManager, Kernel, RuntimeServerInterface } from "@pristine-ts/core";
2
2
  import { LogHandlerInterface } from "@pristine-ts/logging";
3
+ import { CorsRequestHandler } from "../cors/cors-request.handler";
3
4
  /**
4
5
  * Per-request override of the start-time bound port/address pair. Both are optional — when
5
6
  * absent, the server falls back to the values resolved from `pristine.http.kernel-server.*`
@@ -37,6 +38,7 @@ export declare class KernelHttpServer implements RuntimeServerInterface {
37
38
  private readonly logHandler;
38
39
  private readonly kernel;
39
40
  private readonly eventIdManager;
41
+ private readonly corsRequestHandler;
40
42
  /**
41
43
  * Identifier reported back via `RuntimeServerInterface.name`. Switches to `"https"` when
42
44
  * TLS config is present, mainly so log lines and `pristine info`-style introspection make
@@ -54,7 +56,7 @@ export declare class KernelHttpServer implements RuntimeServerInterface {
54
56
  * @private
55
57
  */
56
58
  private readonly connections;
57
- constructor(defaultAddress: string, defaultPort: number, defaultTlsKeyPath: string, defaultTlsCertPath: string, logHandler: LogHandlerInterface, kernel: Kernel, eventIdManager: EventIdManager);
59
+ constructor(defaultAddress: string, defaultPort: number, defaultTlsKeyPath: string, defaultTlsCertPath: string, logHandler: LogHandlerInterface, kernel: Kernel, eventIdManager: EventIdManager, corsRequestHandler: CorsRequestHandler);
58
60
  /**
59
61
  * Binds the server. Resolves once the underlying server is listening; rejects on bind error
60
62
  * (port in use, EACCES, etc).
@@ -71,6 +73,12 @@ export declare class KernelHttpServer implements RuntimeServerInterface {
71
73
  stop(options?: {
72
74
  drainTimeoutMs?: number;
73
75
  }): Promise<void>;
76
+ /**
77
+ * The address the underlying server is bound to, or `null` when not listening. For a TCP socket
78
+ * this is `{address, family, port}` — useful when binding to port `0` (ephemeral port) and the
79
+ * caller needs to discover the assigned port afterwards.
80
+ */
81
+ getAddress(): import("net").AddressInfo | string | null;
74
82
  /**
75
83
  * Reads the body, builds a Pristine `Request`, dispatches via the kernel, writes the
76
84
  * resulting `Response`. All errors are caught and turned into a 500 response so the
@@ -97,6 +105,10 @@ export declare class KernelHttpServer implements RuntimeServerInterface {
97
105
  * Writes a Pristine `Response` (or any object — handlers that return raw JSON serialize as-is)
98
106
  * back to the underlying `ServerResponse`. Default status is 200, default content-type is
99
107
  * `application/json` when the body is non-string.
108
+ *
109
+ * `corsHeaders` (empty when CORS is off or the Origin isn't allow-listed) is applied AFTER the
110
+ * response's own headers so the `Access-Control-*` set is authoritative and a handler can't
111
+ * accidentally clobber it.
100
112
  * @private
101
113
  */
102
114
  private writeResponse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pristine-ts/http",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "",
5
5
  "module": "dist/lib/esm/http.module.js",
6
6
  "main": "dist/lib/cjs/http.module.js",
@@ -16,14 +16,14 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "@pristine-ts/class-validator": "^2.0.4",
19
- "@pristine-ts/cli": "^4.0.2",
20
- "@pristine-ts/common": "^4.0.2",
21
- "@pristine-ts/configuration": "^4.0.2",
22
- "@pristine-ts/core": "^4.0.2",
23
- "@pristine-ts/data-mapping": "^4.0.2",
24
- "@pristine-ts/logging": "^4.0.2",
25
- "@pristine-ts/observability": "^4.0.2",
26
- "@pristine-ts/validation": "^4.0.2",
19
+ "@pristine-ts/cli": "^4.0.3",
20
+ "@pristine-ts/common": "^4.0.3",
21
+ "@pristine-ts/configuration": "^4.0.3",
22
+ "@pristine-ts/core": "^4.0.3",
23
+ "@pristine-ts/data-mapping": "^4.0.3",
24
+ "@pristine-ts/logging": "^4.0.3",
25
+ "@pristine-ts/observability": "^4.0.3",
26
+ "@pristine-ts/validation": "^4.0.3",
27
27
  "tsyringe": "^4.8.0"
28
28
  },
29
29
  "author": "",
@@ -65,7 +65,7 @@
65
65
  "src/*.{js,ts}"
66
66
  ]
67
67
  },
68
- "gitHead": "ff1d17406b92dc73891b7091840c72dcfe84aea6",
68
+ "gitHead": "c301b556f877d7d16e6e5e8faf049c9e3b99e15a",
69
69
  "repository": {
70
70
  "type": "git",
71
71
  "url": "https://github.com/magieno/pristine-ts.git",