@qilin-agent/client-connection 3.1.3 → 3.1.5
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.i18n.yaml +14 -14
- package/lib/index.js +58 -22
- package/lib/types/api-request-trust.d.ts +16 -9
- package/package.json +16 -16
package/README.i18n.yaml
CHANGED
|
@@ -5,33 +5,33 @@
|
|
|
5
5
|
/:
|
|
6
6
|
en: 13b131457d03a224
|
|
7
7
|
zh: 541bd85b21259ff3
|
|
8
|
-
/qilin-client-connection:
|
|
9
|
-
en:
|
|
10
|
-
zh:
|
|
11
|
-
/qilin-client-connection/summary:
|
|
8
|
+
/qilin-agent-client-connection:
|
|
9
|
+
en: eec5c8484d97c773
|
|
10
|
+
zh: 86905b0501d26e73
|
|
11
|
+
/qilin-agent-client-connection/summary:
|
|
12
12
|
en: a83b5e9fd1968062
|
|
13
13
|
zh: ac68628e185a4bff
|
|
14
|
-
/qilin-client-connection/table-of-contents:
|
|
14
|
+
/qilin-agent-client-connection/table-of-contents:
|
|
15
15
|
en: 18792bf2fb4f8030
|
|
16
16
|
zh: 84c63acc7e0099ae
|
|
17
|
-
/qilin-client-connection/use-this-package:
|
|
17
|
+
/qilin-agent-client-connection/use-this-package:
|
|
18
18
|
en: ef04350968741ba3
|
|
19
19
|
zh: abcded86153ddd9b
|
|
20
|
-
/qilin-client-connection/browser-authentication-and-request-trust:
|
|
20
|
+
/qilin-agent-client-connection/browser-authentication-and-request-trust:
|
|
21
21
|
en: 73a9f2fd2c2d559e
|
|
22
22
|
zh: 4b629185fecee159
|
|
23
|
-
/qilin-client-connection/connection-generation:
|
|
24
|
-
en:
|
|
25
|
-
zh:
|
|
26
|
-
/qilin-client-connection/model-experience:
|
|
23
|
+
/qilin-agent-client-connection/connection-generation:
|
|
24
|
+
en: d260c2ac53a86c8f
|
|
25
|
+
zh: 3f1de43f27afd85e
|
|
26
|
+
/qilin-agent-client-connection/model-experience:
|
|
27
27
|
en: 88bbc33fe9ae9df8
|
|
28
28
|
zh: bfbab6d51b4abd40
|
|
29
|
-
/qilin-client-connection/model-experience/kv-cache-effect:
|
|
29
|
+
/qilin-agent-client-connection/model-experience/kv-cache-effect:
|
|
30
30
|
en: ca75c51c89c2c9b0
|
|
31
31
|
zh: de54f3467ef3b9be
|
|
32
|
-
/qilin-client-connection/known-limitations-and-deferred-work:
|
|
32
|
+
/qilin-agent-client-connection/known-limitations-and-deferred-work:
|
|
33
33
|
en: 21d90520c2e5d4b9
|
|
34
34
|
zh: c9ae88395b272977
|
|
35
|
-
/qilin-client-connection/known-limitations-and-deferred-work/dev-note:
|
|
35
|
+
/qilin-agent-client-connection/known-limitations-and-deferred-work/dev-note:
|
|
36
36
|
en: e67f633dc6fe7773
|
|
37
37
|
zh: e88aeb617ffb74ea
|
package/lib/index.js
CHANGED
|
@@ -128,23 +128,27 @@ function isLoopbackHostname(hostname) {
|
|
|
128
128
|
* the attacker's domain while the socket reaches this server) and cross-site
|
|
129
129
|
* requests fired from a malicious page. The Host fence binds every request,
|
|
130
130
|
* browser-looking or not: over plain HTTP a browser attaches neither Origin
|
|
131
|
-
* nor Fetch-Metadata to reads (images and navigations — those
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* belongs to the webserver config, and this fence is not an auth layer.
|
|
131
|
+
* nor Fetch-Metadata to reads (images and navigations — those headers go only
|
|
132
|
+
* to trustworthy destinations), so an unmarked request may still be a rebound
|
|
133
|
+
* browser read and Host is the one header rebinding cannot forge. Non-browser
|
|
134
|
+
* and remote clients pass the same fence via loopback, the listener's bind IP
|
|
135
|
+
* literal, or a declared `trustedHosts` authority. Binding policy belongs to
|
|
136
|
+
* the webserver config; this fence never establishes identity.
|
|
138
137
|
*/
|
|
139
138
|
function header$1(headers, name) {
|
|
140
139
|
if (headers instanceof Headers) return headers.get(name) ?? void 0;
|
|
141
140
|
const value = headers[name];
|
|
142
141
|
return typeof value === "string" ? value : void 0;
|
|
143
142
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Normalized URL of a bare authority (hostname lowercased, default port
|
|
145
|
+
* stripped, IPv6 bracketed), or undefined when unparsable.
|
|
146
|
+
* @param authority - `host` or `host:port` text.
|
|
147
|
+
* @param scheme - special scheme to parse under; its default port is the one stripped.
|
|
148
|
+
*/
|
|
149
|
+
function parseAuthority(authority, scheme = "http:") {
|
|
146
150
|
try {
|
|
147
|
-
return new URL(
|
|
151
|
+
return new URL(`${scheme}//${authority}`);
|
|
148
152
|
} catch {
|
|
149
153
|
return;
|
|
150
154
|
}
|
|
@@ -176,43 +180,74 @@ function assertTrustedAuthority(entry) {
|
|
|
176
180
|
* shapes like `host:port ` as port-less.
|
|
177
181
|
*/
|
|
178
182
|
function canonicalAuthority(entry, entryUrl) {
|
|
179
|
-
const port = entryUrl.port !== "" ? entryUrl.port : new URL(
|
|
183
|
+
const port = entryUrl.port !== "" ? entryUrl.port : new URL(`${entryUrl.protocol === "http:" ? "https:" : "http:"}//${entry}`).port;
|
|
180
184
|
return port === "" ? entryUrl.hostname : `${entryUrl.hostname}:${port}`;
|
|
181
185
|
}
|
|
182
186
|
/**
|
|
183
187
|
* Whether the request authority matches a `trustedHosts` entry. An entry with
|
|
184
188
|
* an explicit port matches that exact authority; a port-less entry matches the
|
|
185
|
-
* hostname on any port
|
|
186
|
-
*
|
|
187
|
-
* normalization, so case and a redundant `:80` never decide trust.
|
|
189
|
+
* hostname on any port. Both sides compare through WHATWG normalization,
|
|
190
|
+
* so case and a redundant scheme-default port never decide trust.
|
|
188
191
|
*/
|
|
189
|
-
function isTrustedAuthority(hostUrl, trustedHosts) {
|
|
192
|
+
function isTrustedAuthority(hostUrl, trustedHosts, protocol) {
|
|
190
193
|
return trustedHosts.some((entry) => {
|
|
191
|
-
const entryUrl = parseAuthority(entry);
|
|
194
|
+
const entryUrl = parseAuthority(entry, protocol);
|
|
192
195
|
if (entryUrl === void 0) return false;
|
|
193
196
|
return canonicalAuthority(entry, entryUrl) === entryUrl.hostname ? entryUrl.hostname === hostUrl.hostname : entryUrl.host === hostUrl.host;
|
|
194
197
|
});
|
|
195
198
|
}
|
|
196
199
|
/**
|
|
200
|
+
* Whether a parsed authority names the deployment's own bind address. A browser
|
|
201
|
+
* dialing that literal sends it as Host, and accepting it grants nothing
|
|
202
|
+
* further: the literal is no rebinding target, the Origin and cross-site checks
|
|
203
|
+
* still apply, and it stays outside `trustedHosts`. Matching ignores the port,
|
|
204
|
+
* like a port-less entry, and any `%zone` id (an interface selector, not
|
|
205
|
+
* address text).
|
|
206
|
+
*/
|
|
207
|
+
function isBindAddressAuthority(hostUrl, bindHost) {
|
|
208
|
+
if (bindHost === void 0) return false;
|
|
209
|
+
const zoneAt = bindHost.indexOf("%");
|
|
210
|
+
const address = zoneAt === -1 ? bindHost : bindHost.slice(0, zoneAt);
|
|
211
|
+
const bindUrl = parseAuthority(address.includes(":") ? `[${address}]` : address);
|
|
212
|
+
return bindUrl !== void 0 && bindUrl.hostname === hostUrl.hostname;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
197
215
|
* Decide whether one /api request may reach the RPC bridge.
|
|
198
216
|
* @param request - Node HTTP or Fetch request facts (headers).
|
|
199
217
|
* @param trustedHosts - non-loopback authorities this deployment serves: exact `host:port`, or port-less `host` matching any port.
|
|
200
|
-
* @
|
|
218
|
+
* @param bindHost - the listener's own bind IP literal, accepted on any port independently of `trustedHosts`.
|
|
219
|
+
* @param protocol - listener protocol for default Host ports; HTTP when no Web carrier is mounted.
|
|
220
|
+
* @returns true when the Host is ours (loopback, the bind address, or trusted) and any attached browser markers are same-origin.
|
|
201
221
|
*/
|
|
202
|
-
function isTrustedApiRequest(request, trustedHosts) {
|
|
222
|
+
function isTrustedApiRequest(request, trustedHosts, bindHost, protocol = "http:") {
|
|
203
223
|
const host = header$1(request.headers, "host");
|
|
204
224
|
if (host === void 0) return false;
|
|
205
|
-
const hostUrl = parseAuthority(host);
|
|
225
|
+
const hostUrl = parseAuthority(host, protocol);
|
|
206
226
|
if (hostUrl === void 0) return false;
|
|
207
|
-
if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts)) return false;
|
|
227
|
+
if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts, protocol) && !isBindAddressAuthority(hostUrl, bindHost)) return false;
|
|
208
228
|
if (header$1(request.headers, "sec-fetch-site") === "cross-site") return false;
|
|
209
229
|
const origin = header$1(request.headers, "origin");
|
|
210
230
|
if (origin === void 0) return true;
|
|
231
|
+
return isSameAuthorityOrigin(origin, host);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Compare Host under the Origin's HTTP(S) scheme so default ports normalize
|
|
235
|
+
* consistently. The transport may remain HTTP behind an HTTPS proxy.
|
|
236
|
+
* @param origin - verbatim Origin header value.
|
|
237
|
+
* @param host - verbatim Host header value already accepted by the Host fence.
|
|
238
|
+
* @returns true only when the Origin is this listener's own `http(s)` authority.
|
|
239
|
+
*/
|
|
240
|
+
function isSameAuthorityOrigin(origin, host) {
|
|
241
|
+
let originUrl;
|
|
211
242
|
try {
|
|
212
|
-
|
|
243
|
+
originUrl = new URL(origin);
|
|
213
244
|
} catch {
|
|
214
245
|
return false;
|
|
215
246
|
}
|
|
247
|
+
const scheme = originUrl.protocol === "https:" ? "https:" : originUrl.protocol === "http:" ? "http:" : void 0;
|
|
248
|
+
if (scheme === void 0) return false;
|
|
249
|
+
const hostUrl = parseAuthority(host, scheme);
|
|
250
|
+
return hostUrl !== void 0 && originUrl.host === hostUrl.host;
|
|
216
251
|
}
|
|
217
252
|
//#endregion
|
|
218
253
|
//#region lib/types/web-entry.js
|
|
@@ -667,7 +702,8 @@ var HostConnectionService = class extends Service {
|
|
|
667
702
|
* cookie minted by the launch-token exchange.
|
|
668
703
|
*/
|
|
669
704
|
requestRejection(request) {
|
|
670
|
-
|
|
705
|
+
const carrier = this.ctx.get("webServer");
|
|
706
|
+
if (!isTrustedApiRequest(request, this.trustedHosts, carrier?.host, carrier?.protocol ?? "http:")) return 403;
|
|
671
707
|
const authority = this.sessionAuthority;
|
|
672
708
|
if (authority === void 0) return this.browserAuth.isAuthenticated(request) ? void 0 : 401;
|
|
673
709
|
if (authority.isPublicApiRequest(request)) return void 0;
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
* the attacker's domain while the socket reaches this server) and cross-site
|
|
5
5
|
* requests fired from a malicious page. The Host fence binds every request,
|
|
6
6
|
* browser-looking or not: over plain HTTP a browser attaches neither Origin
|
|
7
|
-
* nor Fetch-Metadata to reads (images and navigations — those
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* belongs to the webserver config, and this fence is not an auth layer.
|
|
7
|
+
* nor Fetch-Metadata to reads (images and navigations — those headers go only
|
|
8
|
+
* to trustworthy destinations), so an unmarked request may still be a rebound
|
|
9
|
+
* browser read and Host is the one header rebinding cannot forge. Non-browser
|
|
10
|
+
* and remote clients pass the same fence via loopback, the listener's bind IP
|
|
11
|
+
* literal, or a declared `trustedHosts` authority. Binding policy belongs to
|
|
12
|
+
* the webserver config; this fence never establishes identity.
|
|
14
13
|
*/
|
|
15
14
|
import type { ConnectionTrustRequest } from './rpc.ts';
|
|
16
15
|
/**
|
|
@@ -28,11 +27,19 @@ import type { ConnectionTrustRequest } from './rpc.ts';
|
|
|
28
27
|
* @param entry - the configured value, verbatim.
|
|
29
28
|
*/
|
|
30
29
|
export declare function assertTrustedAuthority(entry: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Classify an authority already accepted by {@link assertTrustedAuthority}.
|
|
32
|
+
* @param entry - the configured authority, verbatim.
|
|
33
|
+
* @returns true unless the entry parses as a loopback hostname.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isRemoteAuthority(entry: string): boolean;
|
|
31
36
|
/**
|
|
32
37
|
* Decide whether one /api request may reach the RPC bridge.
|
|
33
38
|
* @param request - Node HTTP or Fetch request facts (headers).
|
|
34
39
|
* @param trustedHosts - non-loopback authorities this deployment serves: exact `host:port`, or port-less `host` matching any port.
|
|
35
|
-
* @
|
|
40
|
+
* @param bindHost - the listener's own bind IP literal, accepted on any port independently of `trustedHosts`.
|
|
41
|
+
* @param protocol - listener protocol for default Host ports; HTTP when no Web carrier is mounted.
|
|
42
|
+
* @returns true when the Host is ours (loopback, the bind address, or trusted) and any attached browser markers are same-origin.
|
|
36
43
|
*/
|
|
37
|
-
export declare function isTrustedApiRequest(request: ConnectionTrustRequest, trustedHosts: readonly string[]): boolean;
|
|
44
|
+
export declare function isTrustedApiRequest(request: ConnectionTrustRequest, trustedHosts: readonly string[], bindHost?: string, protocol?: 'http:' | 'https:'): boolean;
|
|
38
45
|
//# sourceMappingURL=api-request-trust.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/client-connection",
|
|
3
3
|
"description": "Authenticated RPC transport and generation lifecycle",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"zod": "^4.4.3",
|
|
41
|
-
"@qilin-agent/credentials": "3.1.
|
|
41
|
+
"@qilin-agent/credentials": "3.1.5",
|
|
42
42
|
"@qilin-agent/schemastery": "~3.18.3"
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
@@ -47,22 +47,22 @@
|
|
|
47
47
|
"lib/types/**/*.d.ts"
|
|
48
48
|
],
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@qilin-agent/
|
|
51
|
-
"@qilin-agent/
|
|
50
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
51
|
+
"@qilin-agent/scope": "3.1.5"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@qilin-agent/kylin": "~4.0.3",
|
|
55
|
-
"@qilin-agent/
|
|
56
|
-
"@qilin-agent/
|
|
57
|
-
"@qilin-agent/host-webserver": "3.1.
|
|
58
|
-
"@qilin-agent/llm": "3.1.
|
|
59
|
-
"@qilin-agent/
|
|
60
|
-
"@qilin-agent/
|
|
61
|
-
"@qilin-agent/
|
|
62
|
-
"@qilin-agent/tool-todo": "3.1.
|
|
63
|
-
"@qilin-agent/
|
|
64
|
-
"@qilin-agent/
|
|
65
|
-
"@qilin-agent/settings": "3.1.
|
|
66
|
-
"@qilin-agent/host-directory-picker": "3.1.
|
|
55
|
+
"@qilin-agent/brand": "3.1.5",
|
|
56
|
+
"@qilin-agent/attachment": "3.1.5",
|
|
57
|
+
"@qilin-agent/host-webserver": "3.1.5",
|
|
58
|
+
"@qilin-agent/llm": "3.1.5",
|
|
59
|
+
"@qilin-agent/session": "3.1.5",
|
|
60
|
+
"@qilin-agent/typert-protocol": "3.1.5",
|
|
61
|
+
"@qilin-agent/scope": "3.1.5",
|
|
62
|
+
"@qilin-agent/tool-todo": "3.1.5",
|
|
63
|
+
"@qilin-agent/commands": "3.1.5",
|
|
64
|
+
"@qilin-agent/util-values": "3.1.5",
|
|
65
|
+
"@qilin-agent/settings": "3.1.5",
|
|
66
|
+
"@qilin-agent/host-directory-picker": "3.1.5"
|
|
67
67
|
}
|
|
68
68
|
}
|