@queryweave/node 0.1.0-alpha.1 → 0.1.0-beta.2
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 +6 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Node.js request adapters that bridge into `@queryweave/server`.
|
|
4
4
|
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @queryweave/node
|
|
7
|
+
```
|
|
8
|
+
|
|
5
9
|
```ts
|
|
6
10
|
import { readNodeQuery, resolveNodeRequestUrl } from "@queryweave/node";
|
|
7
11
|
|
|
@@ -12,3 +16,5 @@ resolveNodeRequestUrl(request, { trustForwardedHeaders: true });
|
|
|
12
16
|
Decoding lives in one place: this package only translates Node primitives. Forwarded headers are
|
|
13
17
|
ignored unless you opt in. No Express, Fastify, NestJS, Hono, or frontend framework dependency is
|
|
14
18
|
included.
|
|
19
|
+
|
|
20
|
+
Documentation: https://queryweave-docs.vercel.app/adapters/node/
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { DecodeResult, QueryModel, QueryModelValues, QueryParamDefinitions, Quer
|
|
|
11
11
|
interface ResolveNodeRequestUrlOptions {
|
|
12
12
|
/** Explicit authority, overriding request headers. */
|
|
13
13
|
readonly host?: string | undefined;
|
|
14
|
-
/** Explicit scheme without `:`, overriding request headers. */
|
|
14
|
+
/** Explicit scheme without `:`, overriding request headers and the socket. */
|
|
15
15
|
readonly protocol?: string | undefined;
|
|
16
16
|
/** Read `x-forwarded-host` and `x-forwarded-proto` when present. Off by default. */
|
|
17
17
|
readonly trustForwardedHeaders?: boolean | undefined;
|
|
@@ -20,7 +20,14 @@ interface ResolveNodeRequestUrlOptions {
|
|
|
20
20
|
interface NodeRequestQuerySource extends QuerySource {
|
|
21
21
|
readonly request: IncomingMessage;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the absolute URL a Node request was made against.
|
|
25
|
+
*
|
|
26
|
+
* The authority comes from the `host` header, or from `:authority` on an HTTP/2 request; the
|
|
27
|
+
* scheme from `:scheme` or the socket's TLS state. Forwarded headers are read only when
|
|
28
|
+
* `trustForwardedHeaders` is set. Never throws: a request whose headers cannot form a URL still
|
|
29
|
+
* resolves, against the placeholder host.
|
|
30
|
+
*/
|
|
24
31
|
declare function resolveNodeRequestUrl(request: IncomingMessage, options?: ResolveNodeRequestUrlOptions): URL;
|
|
25
32
|
/** Decode the query of a Node request with a model. */
|
|
26
33
|
declare function readNodeQuery<TDefs extends QueryParamDefinitions>(request: IncomingMessage, model: QueryModel<TDefs>, options?: ResolveNodeRequestUrlOptions): DecodeResult<QueryModelValues<TDefs>>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;UAmBiB;;WAEN;;WAEA;;WAEA;;;UAIM,+BAA+B;WACrC,SAAS
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;UAmBiB;;WAEN;;WAEA;;WAEA;;;UAIM,+BAA+B;WACrC,SAAS;;;;;;;;;;iBAoEJ,sBACd,SAAS,iBACT,UAAS,+BACR;;iBAoCa,cAAc,cAAc,uBAC1C,SAAS,iBACT,OAAO,WAAW,QAClB,UAAS,+BACR,aAAa,iBAAiB;;iBAKX,mBAAmB,cAAc,uBACrD,SAAS,iBACT,OAAO,WAAW,QAClB,UAAS,+BACR,QAAQ,aAAa,iBAAiB;;iBAKzB,sBACd,SAAS,iBACT,UAAS,+BACR"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readUrlQuery, readUrlQueryAsync } from "@queryweave/server";
|
|
|
2
2
|
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const fallbackHost = "queryweave.invalid";
|
|
5
|
+
const absoluteForm = /^[a-z][a-z0-9+.-]*:\/\//iu;
|
|
5
6
|
function firstHeaderValue(value) {
|
|
6
7
|
if (value === void 0) return;
|
|
7
8
|
const raw = typeof value === "string" ? value : value[0];
|
|
@@ -9,13 +10,63 @@ function firstHeaderValue(value) {
|
|
|
9
10
|
const first = raw.split(",")[0]?.trim();
|
|
10
11
|
return first === void 0 || first === "" ? void 0 : first;
|
|
11
12
|
}
|
|
12
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The request target as the client sent it.
|
|
15
|
+
*
|
|
16
|
+
* Frameworks that mount routers rewrite `url` to strip the mount prefix and keep the original in
|
|
17
|
+
* `originalUrl`; the original is the one a canonical URL should be built from.
|
|
18
|
+
*/
|
|
19
|
+
function requestTarget(request) {
|
|
20
|
+
const original = request.originalUrl;
|
|
21
|
+
if (typeof original === "string" && original !== "") return original;
|
|
22
|
+
return request.url ?? "/";
|
|
23
|
+
}
|
|
24
|
+
function isEncrypted(request) {
|
|
25
|
+
return request.socket?.encrypted === true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compose an origin-form target onto an authority without letting the target reach the host.
|
|
29
|
+
*
|
|
30
|
+
* `new URL("//evil.example/p", base)` would treat the path as protocol-relative; setting the
|
|
31
|
+
* path and query separately keeps a `//` path on the request's own host. A malformed authority
|
|
32
|
+
* falls back to the placeholder host rather than throwing.
|
|
33
|
+
*/
|
|
34
|
+
function composeUrl(protocol, host, target) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(`${protocol}://${host}/`);
|
|
38
|
+
} catch {
|
|
39
|
+
url = new URL(`http://${fallbackHost}/`);
|
|
40
|
+
}
|
|
41
|
+
const hash = target.indexOf("#");
|
|
42
|
+
const withoutHash = hash === -1 ? target : target.slice(0, hash);
|
|
43
|
+
const query = withoutHash.indexOf("?");
|
|
44
|
+
const path = query === -1 ? withoutHash : withoutHash.slice(0, query);
|
|
45
|
+
url.pathname = path.startsWith("/") ? path : `/${path}`;
|
|
46
|
+
url.search = query === -1 ? "" : withoutHash.slice(query + 1);
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the absolute URL a Node request was made against.
|
|
51
|
+
*
|
|
52
|
+
* The authority comes from the `host` header, or from `:authority` on an HTTP/2 request; the
|
|
53
|
+
* scheme from `:scheme` or the socket's TLS state. Forwarded headers are read only when
|
|
54
|
+
* `trustForwardedHeaders` is set. Never throws: a request whose headers cannot form a URL still
|
|
55
|
+
* resolves, against the placeholder host.
|
|
56
|
+
*/
|
|
13
57
|
function resolveNodeRequestUrl(request, options = {}) {
|
|
14
58
|
const headers = request.headers;
|
|
15
59
|
const trusted = options.trustForwardedHeaders === true;
|
|
16
|
-
const host = options.host ?? (trusted ? firstHeaderValue(headers["x-forwarded-host"]) : void 0) ?? firstHeaderValue(headers.host) ?? fallbackHost;
|
|
17
|
-
const protocol = options.protocol ?? (trusted ? firstHeaderValue(headers["x-forwarded-proto"]) : void 0) ?? "http";
|
|
18
|
-
|
|
60
|
+
const host = options.host ?? (trusted ? firstHeaderValue(headers["x-forwarded-host"]) : void 0) ?? firstHeaderValue(headers.host) ?? firstHeaderValue(headers[":authority"]) ?? fallbackHost;
|
|
61
|
+
const protocol = options.protocol ?? (trusted ? firstHeaderValue(headers["x-forwarded-proto"]) : void 0) ?? firstHeaderValue(headers[":scheme"]) ?? (isEncrypted(request) ? "https" : "http");
|
|
62
|
+
const target = requestTarget(request);
|
|
63
|
+
if (absoluteForm.test(target)) try {
|
|
64
|
+
const url = new URL(target);
|
|
65
|
+
if (options.host !== void 0) url.host = options.host;
|
|
66
|
+
if (options.protocol !== void 0) url.protocol = options.protocol;
|
|
67
|
+
return url;
|
|
68
|
+
} catch {}
|
|
69
|
+
return composeUrl(protocol, host, target);
|
|
19
70
|
}
|
|
20
71
|
/** Decode the query of a Node request with a model. */
|
|
21
72
|
function readNodeQuery(request, model, options = {}) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\n\nimport type {\n DecodeResult,\n QueryModel,\n QueryModelValues,\n QueryParamDefinitions,\n QuerySource,\n} from \"@queryweave/core\";\nimport { readUrlQuery, readUrlQueryAsync } from \"@queryweave/server\";\n\n/**\n * Node.js request adapters.\n *\n * This package only bridges Node primitives into `@queryweave/server`; decoding stays in one\n * place, and no HTTP framework is required.\n */\n\n/** Options accepted by {@link resolveNodeRequestUrl}. */\nexport interface ResolveNodeRequestUrlOptions {\n /** Explicit authority, overriding request headers. */\n readonly host?: string | undefined;\n /** Explicit scheme without `:`, overriding request headers. */\n readonly protocol?: string | undefined;\n /** Read `x-forwarded-host` and `x-forwarded-proto` when present. Off by default. */\n readonly trustForwardedHeaders?: boolean | undefined;\n}\n\n/** A read-only source backed by a Node request. */\nexport interface NodeRequestQuerySource extends QuerySource {\n readonly request: IncomingMessage;\n}\n\nconst fallbackHost = \"queryweave.invalid\";\n\nfunction firstHeaderValue(value: string | readonly string[] | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const raw = typeof value === \"string\" ? value : value[0];\n if (raw === undefined) {\n return undefined;\n }\n const first = raw.split(\",\")[0]?.trim();\n return first === undefined || first === \"\" ? undefined : first;\n}\n\n
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\n\nimport type {\n DecodeResult,\n QueryModel,\n QueryModelValues,\n QueryParamDefinitions,\n QuerySource,\n} from \"@queryweave/core\";\nimport { readUrlQuery, readUrlQueryAsync } from \"@queryweave/server\";\n\n/**\n * Node.js request adapters.\n *\n * This package only bridges Node primitives into `@queryweave/server`; decoding stays in one\n * place, and no HTTP framework is required.\n */\n\n/** Options accepted by {@link resolveNodeRequestUrl}. */\nexport interface ResolveNodeRequestUrlOptions {\n /** Explicit authority, overriding request headers. */\n readonly host?: string | undefined;\n /** Explicit scheme without `:`, overriding request headers and the socket. */\n readonly protocol?: string | undefined;\n /** Read `x-forwarded-host` and `x-forwarded-proto` when present. Off by default. */\n readonly trustForwardedHeaders?: boolean | undefined;\n}\n\n/** A read-only source backed by a Node request. */\nexport interface NodeRequestQuerySource extends QuerySource {\n readonly request: IncomingMessage;\n}\n\nconst fallbackHost = \"queryweave.invalid\";\nconst absoluteForm = /^[a-z][a-z0-9+.-]*:\\/\\//iu;\n\nfunction firstHeaderValue(value: string | readonly string[] | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const raw = typeof value === \"string\" ? value : value[0];\n if (raw === undefined) {\n return undefined;\n }\n const first = raw.split(\",\")[0]?.trim();\n return first === undefined || first === \"\" ? undefined : first;\n}\n\n/**\n * The request target as the client sent it.\n *\n * Frameworks that mount routers rewrite `url` to strip the mount prefix and keep the original in\n * `originalUrl`; the original is the one a canonical URL should be built from.\n */\nfunction requestTarget(request: IncomingMessage): string {\n const original = (request as { originalUrl?: unknown }).originalUrl;\n if (typeof original === \"string\" && original !== \"\") {\n return original;\n }\n return request.url ?? \"/\";\n}\n\nfunction isEncrypted(request: IncomingMessage): boolean {\n const socket = (request as { socket?: { encrypted?: unknown } }).socket;\n return socket?.encrypted === true;\n}\n\n/**\n * Compose an origin-form target onto an authority without letting the target reach the host.\n *\n * `new URL(\"//evil.example/p\", base)` would treat the path as protocol-relative; setting the\n * path and query separately keeps a `//` path on the request's own host. A malformed authority\n * falls back to the placeholder host rather than throwing.\n */\nfunction composeUrl(protocol: string, host: string, target: string): URL {\n let url: URL;\n try {\n url = new URL(`${protocol}://${host}/`);\n } catch {\n url = new URL(`http://${fallbackHost}/`);\n }\n const hash = target.indexOf(\"#\");\n const withoutHash = hash === -1 ? target : target.slice(0, hash);\n const query = withoutHash.indexOf(\"?\");\n const path = query === -1 ? withoutHash : withoutHash.slice(0, query);\n url.pathname = path.startsWith(\"/\") ? path : `/${path}`;\n url.search = query === -1 ? \"\" : withoutHash.slice(query + 1);\n return url;\n}\n\n/**\n * Resolve the absolute URL a Node request was made against.\n *\n * The authority comes from the `host` header, or from `:authority` on an HTTP/2 request; the\n * scheme from `:scheme` or the socket's TLS state. Forwarded headers are read only when\n * `trustForwardedHeaders` is set. Never throws: a request whose headers cannot form a URL still\n * resolves, against the placeholder host.\n */\nexport function resolveNodeRequestUrl(\n request: IncomingMessage,\n options: ResolveNodeRequestUrlOptions = {},\n): URL {\n const headers = request.headers;\n const trusted = options.trustForwardedHeaders === true;\n\n const host =\n options.host ??\n (trusted ? firstHeaderValue(headers[\"x-forwarded-host\"]) : undefined) ??\n firstHeaderValue(headers.host) ??\n firstHeaderValue(headers[\":authority\"]) ??\n fallbackHost;\n\n const protocol =\n options.protocol ??\n (trusted ? firstHeaderValue(headers[\"x-forwarded-proto\"]) : undefined) ??\n firstHeaderValue(headers[\":scheme\"]) ??\n (isEncrypted(request) ? \"https\" : \"http\");\n\n const target = requestTarget(request);\n if (absoluteForm.test(target)) {\n try {\n const url = new URL(target);\n if (options.host !== undefined) {\n url.host = options.host;\n }\n if (options.protocol !== undefined) {\n url.protocol = options.protocol;\n }\n return url;\n } catch {\n // Fall through and treat the target as a path.\n }\n }\n return composeUrl(protocol, host, target);\n}\n\n/** Decode the query of a Node request with a model. */\nexport function readNodeQuery<TDefs extends QueryParamDefinitions>(\n request: IncomingMessage,\n model: QueryModel<TDefs>,\n options: ResolveNodeRequestUrlOptions = {},\n): DecodeResult<QueryModelValues<TDefs>> {\n return readUrlQuery(resolveNodeRequestUrl(request, options), model);\n}\n\n/** Decode the query of a Node request, awaiting asynchronous validation. */\nexport async function readNodeQueryAsync<TDefs extends QueryParamDefinitions>(\n request: IncomingMessage,\n model: QueryModel<TDefs>,\n options: ResolveNodeRequestUrlOptions = {},\n): Promise<DecodeResult<QueryModelValues<TDefs>>> {\n return readUrlQueryAsync(resolveNodeRequestUrl(request, options), model);\n}\n\n/** Create a request-scoped read-only source for a Node request. */\nexport function createNodeQuerySource(\n request: IncomingMessage,\n options: ResolveNodeRequestUrlOptions = {},\n): NodeRequestQuerySource {\n return {\n request,\n read: () => resolveNodeRequestUrl(request, options).search,\n };\n}\n"],"mappings":";;;AAiCA,MAAM,eAAe;AACrB,MAAM,eAAe;AAErB,SAAS,iBAAiB,OAAmE;CAC3F,IAAI,UAAU,QACZ;CAEF,MAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,MAAM;CACtD,IAAI,QAAQ,QACV;CAEF,MAAM,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK;CACtC,OAAO,UAAU,UAAa,UAAU,KAAK,SAAY;AAC3D;;;;;;;AAQA,SAAS,cAAc,SAAkC;CACvD,MAAM,WAAY,QAAsC;CACxD,IAAI,OAAO,aAAa,YAAY,aAAa,IAC/C,OAAO;CAET,OAAO,QAAQ,OAAO;AACxB;AAEA,SAAS,YAAY,SAAmC;CAEtD,OADgB,QAAiD,QAClD,cAAc;AAC/B;;;;;;;;AASA,SAAS,WAAW,UAAkB,MAAc,QAAqB;CACvE,IAAI;CACJ,IAAI;EACF,MAAM,IAAI,IAAI,GAAG,SAAS,KAAK,KAAK,EAAE;CACxC,QAAQ;EACN,MAAM,IAAI,IAAI,UAAU,aAAa,EAAE;CACzC;CACA,MAAM,OAAO,OAAO,QAAQ,GAAG;CAC/B,MAAM,cAAc,SAAS,KAAK,SAAS,OAAO,MAAM,GAAG,IAAI;CAC/D,MAAM,QAAQ,YAAY,QAAQ,GAAG;CACrC,MAAM,OAAO,UAAU,KAAK,cAAc,YAAY,MAAM,GAAG,KAAK;CACpE,IAAI,WAAW,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI;CACjD,IAAI,SAAS,UAAU,KAAK,KAAK,YAAY,MAAM,QAAQ,CAAC;CAC5D,OAAO;AACT;;;;;;;;;AAUA,SAAgB,sBACd,SACA,UAAwC,CAAC,GACpC;CACL,MAAM,UAAU,QAAQ;CACxB,MAAM,UAAU,QAAQ,0BAA0B;CAElD,MAAM,OACJ,QAAQ,SACP,UAAU,iBAAiB,QAAQ,mBAAmB,IAAI,WAC3D,iBAAiB,QAAQ,IAAI,KAC7B,iBAAiB,QAAQ,aAAa,KACtC;CAEF,MAAM,WACJ,QAAQ,aACP,UAAU,iBAAiB,QAAQ,oBAAoB,IAAI,WAC5D,iBAAiB,QAAQ,UAAU,MAClC,YAAY,OAAO,IAAI,UAAU;CAEpC,MAAM,SAAS,cAAc,OAAO;CACpC,IAAI,aAAa,KAAK,MAAM,GAC1B,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAC1B,IAAI,QAAQ,SAAS,QACnB,IAAI,OAAO,QAAQ;EAErB,IAAI,QAAQ,aAAa,QACvB,IAAI,WAAW,QAAQ;EAEzB,OAAO;CACT,QAAQ,CAER;CAEF,OAAO,WAAW,UAAU,MAAM,MAAM;AAC1C;;AAGA,SAAgB,cACd,SACA,OACA,UAAwC,CAAC,GACF;CACvC,OAAO,aAAa,sBAAsB,SAAS,OAAO,GAAG,KAAK;AACpE;;AAGA,eAAsB,mBACpB,SACA,OACA,UAAwC,CAAC,GACO;CAChD,OAAO,kBAAkB,sBAAsB,SAAS,OAAO,GAAG,KAAK;AACzE;;AAGA,SAAgB,sBACd,SACA,UAAwC,CAAC,GACjB;CACxB,OAAO;EACL;EACA,YAAY,sBAAsB,SAAS,OAAO,CAAC,CAAC;CACtD;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@queryweave/node",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Node.js request contracts for QueryWeave.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -38,15 +38,15 @@
|
|
|
38
38
|
"provenance": true
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@queryweave/core": "0.1.0-
|
|
42
|
-
"@queryweave/server": "0.1.0-
|
|
41
|
+
"@queryweave/core": "0.1.0-beta.2",
|
|
42
|
+
"@queryweave/server": "0.1.0-beta.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "26.1.1",
|
|
46
46
|
"@queryweave/typescript-config": "0.0.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=22.12.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsdown",
|