@deepseek-ai/dsh-client-connection 0.2.0-rc.2 → 0.2.1-alpha.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.i18n.yaml +8 -8
- package/README.md +6 -6
- package/README.zh.md +6 -6
- package/lib/index.js +94 -38
- package/lib/types/api-request-trust.d.ts +16 -9
- package/lib/types/browser-auth.d.ts +7 -2
- package/lib/types/index.d.ts +3 -5
- package/lib/types/rpc-host.d.ts +5 -2
- package/lib/types/rpc.d.ts +7 -0
- package/package.json +14 -14
package/README.i18n.yaml
CHANGED
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
en: 13b54969ff1d00a4
|
|
19
19
|
zh: 4d2ffff0445472ed
|
|
20
20
|
/deepseek-ai-dsh-client-connection/browser-authentication-and-request-trust:
|
|
21
|
-
en:
|
|
22
|
-
zh:
|
|
21
|
+
en: 44956be3cc11908a
|
|
22
|
+
zh: d518f765d58f3558
|
|
23
23
|
/deepseek-ai-dsh-client-connection/connection-generation:
|
|
24
|
-
en:
|
|
25
|
-
zh:
|
|
24
|
+
en: 116f94e44c0d87e7
|
|
25
|
+
zh: 59ef48984154d858
|
|
26
26
|
/deepseek-ai-dsh-client-connection/model-experience:
|
|
27
27
|
en: 88bbc33fe9ae9df8
|
|
28
28
|
zh: bfbab6d51b4abd40
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
en: ca75c51c89c2c9b0
|
|
31
31
|
zh: de54f3467ef3b9be
|
|
32
32
|
/deepseek-ai-dsh-client-connection/known-limitations-and-deferred-work:
|
|
33
|
-
en:
|
|
34
|
-
zh:
|
|
33
|
+
en: ad73c3b97f348ad1
|
|
34
|
+
zh: fec950f970112182
|
|
35
35
|
/deepseek-ai-dsh-client-connection/known-limitations-and-deferred-work/dev-note:
|
|
36
|
-
en:
|
|
37
|
-
zh:
|
|
36
|
+
en: e67f633dc6fe7773
|
|
37
|
+
zh: e88aeb617ffb74ea
|
package/README.md
CHANGED
|
@@ -38,9 +38,11 @@ The browser uses HTTP POST for Remote unary calls. API Gateway owns the `/api/re
|
|
|
38
38
|
|
|
39
39
|
Every Host RPC method and WebSocket stream requires one browser session; there is no method-specific loopback tier. Each process mints a random launch token. `dsh-web-app` prints and opens its application URL with `?token=...`, preserving the caller's authority and mount; `frontend-static` delegates root and index requests to `ctx.connection.authorizeIndex`, which accepts that token only on `GET /`, writes an authority-bound signed cookie, and redirects to clean `./`, which drops the token and keeps the request's directory. A missing, expired, malformed, or wrong-authority cookie returns 401 before RPC dispatch. Static assets remain public. The HTTP carrier accepts no query token outside the root exchange and no Authorization-header token.
|
|
40
40
|
|
|
41
|
-
The cookie signing secret is the owner-scoped `client-connection/browser-session` grant record in `ctx.credentials`. The local provider persists it in `$DSH_HOME/.credentials.yaml`; `BrowserAuth` loads or creates the record during Connection activation and retains the secret in memory, so request authentication is synchronous. Deleting or replacing the record takes effect on the next Connection activation. Cookies carry an absolute issue/expiry interval, defaulting to 30 days through `cookieMaxAgeDays`, and bind the normalized hostname plus port in both their deterministic name and signed payload. They are host-only, `Path=/`, `HttpOnly`, and `SameSite=Strict
|
|
41
|
+
The cookie signing secret is the owner-scoped `client-connection/browser-session` grant record in `ctx.credentials`. The local provider persists it in `$DSH_HOME/.credentials.yaml`; `BrowserAuth` loads or creates the record during Connection activation and retains the secret in memory, so request authentication is synchronous. Deleting or replacing the record takes effect on the next Connection activation. Cookies carry an absolute issue/expiry interval, defaulting to 30 days through `cookieMaxAgeDays`, and bind the normalized hostname plus port in both their deterministic name and signed payload. They are host-only, `Path=/`, `HttpOnly`, and `SameSite=Strict`, and carry `Secure` exactly when the listener that received the index request serves TLS; only that listener's own protocol decides, never a `publicUrl` announcement or a forwarded header.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
HTTPS cookie names and signed audiences include the `https:` scheme, so HTTP cookies cannot authenticate to a TLS listener even on the same hostname and port. Default ports normalize under the listener's scheme. Existing HTTP cookie audiences remain unchanged.
|
|
44
|
+
|
|
45
|
+
Before authentication, every request passes `src/api-request-trust.ts`. Its `Host` must be loopback, equal the listener's bind IP literal on any port, or match a `trustedHosts` entry: exact on `host:port`, any port on port-less entries. Host and configured entries use the listener's scheme for WHATWG default-port normalization, not a forwarded header or `Origin`. An attached HTTP(S) `Origin` must name the same authority, with Host parsed under that Origin's scheme to support HTTPS proxies over HTTP upstreams; `sec-fetch-site: cross-site` is refused. Malformed configured authorities fail plugin load. `allowsRemoteAuthorities` reports whether `trustedHosts` permits a non-loopback authority; accepting the bind IP does not change that list. These checks defend DNS rebinding and cross-site browser requests; they never establish identity. A failed Host/Origin check returns 403, while a trusted but unauthenticated request returns 401. `dsh web --host 0.0.0.0` is unsupported. Decision records: [browser request trust](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md) and [browser token authentication](../../../.agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.md).
|
|
44
46
|
|
|
45
47
|
Every admitted request speaks for one Peer, the operator. `ctx.connection.operator` is that `PeerScope`: its `ctx` is a Cordis scope that owns connection-lifetime registrations and is disposed with the Connection. `ctx.connection.admit(request)` runs the trust and authentication checks and answers with the rejection status or the operator; the `/api` route and the Gateway's WebSocket upgrade admit through it, and every RPC handler receives the Peer of its call. `OperatorPeer` is exported so a composition without Connection, such as the Gateway's in-process carrier, owns an operator scope with the same contract.
|
|
46
48
|
|
|
@@ -51,7 +53,7 @@ Authenticated shared HTTP requests pass through the `connection/request` waterfa
|
|
|
51
53
|
|
|
52
54
|
API Gateway Client registers the internal `$events` logical stream as the sole generation source, independently of whether any `$on` listener exists. The Host attaches all incremental listeners in the API Remotes source factory, then sends one `{ type: 'ready', clientId, host: { home } }` item before events. `ConnectionController` publishes that generation and calls `onConnected` only after the ready item arrives, so baseline acquisition cannot race ahead of incremental observation.
|
|
53
55
|
|
|
54
|
-
An ended `$events` stream, a Remote stream error, a non-ready opening item, or a malformed event item invalidates the current generation. A pending handshake logs a slow-Host warning after 3 seconds and logs the readiness timeout and aborts after 15 seconds by default, including time spent waiting for the physical socket. The source must stop delivery, release resources, and settle after cancellation before a replacement starts; late readiness from a cancelled source cannot publish a generation. While the browser reports network availability, the controller publishes `connecting` and retries with 50%–100% jitter under caps of 500ms, 1s, 2s, 4s, 8s, and 10s, continuing at the final cap until recovery. Every retry asks Gateway to replace the physical WebSocket once and reopens `$events`.
|
|
56
|
+
An ended `$events` stream, a Remote stream error, a non-ready opening item, or a malformed event item invalidates the current generation. A pending handshake logs a slow-Host warning after 3 seconds and logs the readiness timeout and aborts after 15 seconds by default, including time spent waiting for the physical socket. The source must stop delivery, release resources, and settle after cancellation before a replacement starts; late readiness from a cancelled source cannot publish a generation. While the browser reports network availability, the controller publishes `connecting` and retries with 50%–100% jitter under caps of 500ms, 1s, 2s, 4s, 8s, and 10s, continuing at the final cap until recovery. Every retry asks Gateway to replace the physical WebSocket once and reopens `$events`.
|
|
55
57
|
|
|
56
58
|
`ctx.connection.reconnect()` interrupts active work, resets the sequence, and starts retry 1 immediately. Browser `offline` aborts active work, publishes `disconnected`, and suspends automatic attempts; the next `online` transition resets the sequence and starts at the 500ms tier. Only a ready item publishes `connected`. Gateway mux owns no independent retry schedule.
|
|
57
59
|
|
|
@@ -72,7 +74,7 @@ None; this package neither assembles nor sends a provider request.
|
|
|
72
74
|
<a id="known-limitations-and-deferred-work"></a>
|
|
73
75
|
|
|
74
76
|
- **Buffered `/api` routes retain each request body in memory** — `maxRequestBodyBytes` (default 300 MiB, sized for the default 200 MiB aggregate image limit after base64 expansion plus envelope headroom) bounds ordinary image and RPC envelopes. Opt-in streaming routes receive backpressured chunks and bypass the aggregate cap; route implementations own persistence, cancellation, and any storage quota.
|
|
75
|
-
- **
|
|
77
|
+
- **A plain-HTTP listener exposes the bearer cookie in transit** — such a deployment cannot mark the browser cookie `Secure`, so serving the same authority over a network can leak the session cookie; configure TLS on the Web carrier ([`dsh-host-webserver`](../../host/webserver/README.md)) to have the cookie marked `Secure` and confined to TLS.
|
|
76
78
|
- **There is no logout operation** — clearing the browser cookie ends one browser session; deleting the owner credential record and restarting `dsh` revokes every session.
|
|
77
79
|
|
|
78
80
|
|
|
@@ -85,5 +87,3 @@ None; this package neither assembles nor sends a provider request.
|
|
|
85
87
|
None.
|
|
86
88
|
|
|
87
89
|
</details>
|
|
88
|
-
|
|
89
|
-
**Runtime invariant:** No companion is published. Browser-session verification reads the credential record asynchronously at the request that authorizes work, while the credentials companion owns record commit-event lifetime. Stream/reconnect sequencing and rpcId round-trip discipline are exercised directly by behavior specs, and route register/dispose symmetry is audited by the webserver companion.
|
package/README.zh.md
CHANGED
|
@@ -38,9 +38,11 @@ kind: "package-reference"
|
|
|
38
38
|
|
|
39
39
|
每个 Host RPC 方法和 WebSocket 流都要求一个浏览器会话,不存在按方法区分的 loopback 层。每个进程生成一个随机启动令牌。`dsh-web-app` 打印并打开带 `?token=...` 的应用 URL,保留调用方的 authority 与挂载;`frontend-static` 把根路径和 index 请求交给 `ctx.connection.authorizeIndex`,后者只在 `GET /` 接受该令牌,写入绑定 authority 的签名 cookie,再重定向到干净的 `./`,移除令牌并保留请求目录。缺失、过期、畸形或 authority 不匹配的 cookie 会在 RPC 分发前得到 401。静态资源保持公开。HTTP 载体不在根路径交换之外接受 query token,也不接受 Authorization header token。
|
|
40
40
|
|
|
41
|
-
cookie 签名密钥是 `ctx.credentials` 中由 `client-connection/browser-session` 拥有的 grant 记录。本地提供方把它持久化到 `$DSH_HOME/.credentials.yaml`;`BrowserAuth` 在 Connection 激活期间加载或创建该记录,并把密钥留在内存中,因此请求认证同步执行。删除或替换该记录会在下一次 Connection 激活时生效。cookie 携带绝对签发与过期区间,`cookieMaxAgeDays` 默认设为 30 天,并在确定性名称与签名 payload 中同时绑定规范化 hostname 和 port。它是 host-only、`Path=/`、`HttpOnly`、`SameSite=Strict
|
|
41
|
+
cookie 签名密钥是 `ctx.credentials` 中由 `client-connection/browser-session` 拥有的 grant 记录。本地提供方把它持久化到 `$DSH_HOME/.credentials.yaml`;`BrowserAuth` 在 Connection 激活期间加载或创建该记录,并把密钥留在内存中,因此请求认证同步执行。删除或替换该记录会在下一次 Connection 激活时生效。cookie 携带绝对签发与过期区间,`cookieMaxAgeDays` 默认设为 30 天,并在确定性名称与签名 payload 中同时绑定规范化 hostname 和 port。它是 host-only、`Path=/`、`HttpOnly`、`SameSite=Strict`;收到该 index 请求的监听器提供 TLS 时才带上 `Secure`,且只由该监听器自身的协议决定,`publicUrl` 声明与转发头都不参与判断。
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
HTTPS cookie 名称与签名 audience 包含 `https:` scheme,因此即使 hostname 和 port 相同,HTTP cookie 也无法通过 TLS 监听器的认证。默认端口按监听器的 scheme 归一化。既有 HTTP cookie audience 保持不变。
|
|
44
|
+
|
|
45
|
+
认证之前,每个请求都经过 `src/api-request-trust.ts`。其 `Host` 必须是 loopback、等于监听器的绑定 IP 字面量(端口不限),或与 `trustedHosts` 条目匹配:带端口的 `host:port` 精确匹配,不带端口的条目匹配任意端口。Host 与配置条目按监听器的 scheme 做 WHATWG 默认端口归一化,不采信转发 header 或 `Origin`。附带的 HTTP(S) `Origin` 必须指向同一 authority;此时按 Origin 的 scheme 解析 Host,以支持 HTTPS 代理转发到 HTTP 上游;`sec-fetch-site: cross-site` 一律拒绝。畸形配置 authority 会让插件加载失败。`allowsRemoteAuthorities` 报告 `trustedHosts` 是否允许非 loopback authority;接受绑定 IP 不会改变该列表。这些检查防御 DNS rebinding 与跨站浏览器请求,绝不建立身份。Host/Origin 校验失败返回 403;Host 可信但未认证的请求返回 401。`dsh web --host 0.0.0.0` 不受支持。决策记录:[浏览器请求信任](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.zh.md)与[浏览器令牌认证](../../../.agents/notes/implemented/architecture/2026-08-24-browser-token-authentication.zh.md)。
|
|
44
46
|
|
|
45
47
|
每个被接纳的请求都代表同一个 Peer——操作者。`ctx.connection.operator` 就是这个 `PeerScope`:其 `ctx` 是拥有连接期注册的 Cordis scope,随 Connection 一起释放。`ctx.connection.admit(request)` 执行信任与认证检查,以拒绝状态或操作者作答;`/api` 路由与 Gateway 的 WebSocket 升级都经它接纳,每个 RPC 处理器都收到本次调用的 Peer。`OperatorPeer` 对外导出,供没有 Connection 的组合(例如 Gateway 的进程内载体)以同一约定拥有一个操作者 scope。
|
|
46
48
|
|
|
@@ -51,7 +53,7 @@ cookie 签名密钥是 `ctx.credentials` 中由 `client-connection/browser-sessi
|
|
|
51
53
|
|
|
52
54
|
API Gateway Client 把内部 `$events` 逻辑流注册为唯一 generation source,与有无 `$on` 订阅无关。Host 在 API Remotes source factory 同步挂好所有增量 listener 后,先发送唯一 `{ type: 'ready', clientId, host: { home } }` 项,再发送事件。`ConnectionController` 仅在收到该 ready 项后发布 generation 并调用 `onConnected`,因此 baseline 不会跑在增量 listener 前面。
|
|
53
55
|
|
|
54
|
-
`$events` 结束、Remote 流报错、收到非 ready 首项或畸形事件项,都会使当前 generation 失效。默认情况下,挂起的握手在 3 秒后记录 Host 响应缓慢告警,在 15 秒后记录就绪超时并中止,包含等待物理 socket 的时间。取消后,source 必须停止投递、释放资源并结束,替换 source 才能启动;已取消 source 迟到的 ready 不能发布 generation。浏览器报告网络可用时,Controller 发布 `connecting`,并在 500ms、1s、2s、4s、8s 与 10s 上限内采用 50%–100% 抖动重试,达到终档后继续尝试直到恢复。每次重试都要求 Gateway 替换一次物理 WebSocket,再重开 `$events`。
|
|
56
|
+
`$events` 结束、Remote 流报错、收到非 ready 首项或畸形事件项,都会使当前 generation 失效。默认情况下,挂起的握手在 3 秒后记录 Host 响应缓慢告警,在 15 秒后记录就绪超时并中止,包含等待物理 socket 的时间。取消后,source 必须停止投递、释放资源并结束,替换 source 才能启动;已取消 source 迟到的 ready 不能发布 generation。浏览器报告网络可用时,Controller 发布 `connecting`,并在 500ms、1s、2s、4s、8s 与 10s 上限内采用 50%–100% 抖动重试,达到终档后继续尝试直到恢复。每次重试都要求 Gateway 替换一次物理 WebSocket,再重开 `$events`。
|
|
55
57
|
|
|
56
58
|
`ctx.connection.reconnect()` 会中断活动工作、重置序列,并立即开始 retry 1。浏览器 `offline` 会中断活动工作、发布 `disconnected` 并暂停自动尝试;下一次 `online` 转换会重置序列并从 500ms 档开始。只有 ready 项会发布 `connected`。Gateway mux 不拥有独立重试调度。
|
|
57
59
|
|
|
@@ -72,7 +74,7 @@ API Gateway Client 把内部 `$events` 逻辑流注册为唯一 generation sourc
|
|
|
72
74
|
<a id="known-limitations-and-deferred-work"></a>
|
|
73
75
|
|
|
74
76
|
- **缓冲型 `/api` 路由会把每个请求体保留在内存里**:`maxRequestBodyBytes`(默认 300 MiB,按默认 200 MiB 图片总量上限经 base64 膨胀加信封余量得出)限制普通图片与 RPC 信封。显式启用的流式路由接收带背压的分块并绕过总量上限;路由实现负责持久化、取消与存储配额。
|
|
75
|
-
-
|
|
77
|
+
- **明文 HTTP 监听器会在传输中暴露 bearer cookie**:此类部署无法给浏览器 cookie 加 `Secure`;若把同一 authority 暴露到网络上,会话 cookie 可能泄露;在 Web 载体([`dsh-host-webserver`](../../host/webserver/README.zh.md))上配置 TLS 即可让 cookie 带上 `Secure` 并只经 TLS 发送。
|
|
76
78
|
- **没有 logout 操作**:清除浏览器 cookie 会结束单个浏览器会话;删除 owner 凭据记录并重启 `dsh` 会撤销全部会话。
|
|
77
79
|
|
|
78
80
|
|
|
@@ -85,5 +87,3 @@ API Gateway Client 把内部 `$events` 逻辑流注册为唯一 generation sourc
|
|
|
85
87
|
无。
|
|
86
88
|
|
|
87
89
|
</details>
|
|
88
|
-
|
|
89
|
-
**运行时不变式:** 不发布伴生入口。浏览器会话验证会在请求授权工作时异步读取凭据记录,而记录的 commit-event 生命周期由 credentials 伴生入口负责;流与重连的时序及 rpcId 往返约束由行为规范直接验证,路由注册与 dispose(资源释放)的对称性由 webserver 伴生入口审计。
|
package/lib/index.js
CHANGED
|
@@ -131,23 +131,27 @@ function isLoopbackHostname(hostname) {
|
|
|
131
131
|
* the attacker's domain while the socket reaches this server) and cross-site
|
|
132
132
|
* requests fired from a malicious page. The Host fence binds every request,
|
|
133
133
|
* browser-looking or not: over plain HTTP a browser attaches neither Origin
|
|
134
|
-
* nor Fetch-Metadata to reads (images and navigations — those
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* belongs to the webserver config, and this fence is not an auth layer.
|
|
134
|
+
* nor Fetch-Metadata to reads (images and navigations — those headers go only
|
|
135
|
+
* to trustworthy destinations), so an unmarked request may still be a rebound
|
|
136
|
+
* browser read and Host is the one header rebinding cannot forge. Non-browser
|
|
137
|
+
* and remote clients pass the same fence via loopback, the listener's bind IP
|
|
138
|
+
* literal, or a declared `trustedHosts` authority. Binding policy belongs to
|
|
139
|
+
* the webserver config; this fence never establishes identity.
|
|
141
140
|
*/
|
|
142
141
|
function header$1(headers, name) {
|
|
143
142
|
if (headers instanceof Headers) return headers.get(name) ?? void 0;
|
|
144
143
|
const value = headers[name];
|
|
145
144
|
return typeof value === "string" ? value : void 0;
|
|
146
145
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Normalized URL of a bare authority (hostname lowercased, default port
|
|
148
|
+
* stripped, IPv6 bracketed), or undefined when unparsable.
|
|
149
|
+
* @param authority - `host` or `host:port` text.
|
|
150
|
+
* @param scheme - special scheme to parse under; its default port is the one stripped.
|
|
151
|
+
*/
|
|
152
|
+
function parseAuthority(authority, scheme = "http:") {
|
|
149
153
|
try {
|
|
150
|
-
return new URL(
|
|
154
|
+
return new URL(`${scheme}//${authority}`);
|
|
151
155
|
} catch {
|
|
152
156
|
return;
|
|
153
157
|
}
|
|
@@ -179,43 +183,83 @@ function assertTrustedAuthority(entry) {
|
|
|
179
183
|
* shapes like `host:port ` as port-less.
|
|
180
184
|
*/
|
|
181
185
|
function canonicalAuthority(entry, entryUrl) {
|
|
182
|
-
const port = entryUrl.port !== "" ? entryUrl.port : new URL(
|
|
186
|
+
const port = entryUrl.port !== "" ? entryUrl.port : new URL(`${entryUrl.protocol === "http:" ? "https:" : "http:"}//${entry}`).port;
|
|
183
187
|
return port === "" ? entryUrl.hostname : `${entryUrl.hostname}:${port}`;
|
|
184
188
|
}
|
|
185
189
|
/**
|
|
186
190
|
* Whether the request authority matches a `trustedHosts` entry. An entry with
|
|
187
191
|
* an explicit port matches that exact authority; a port-less entry matches the
|
|
188
|
-
* hostname on any port
|
|
189
|
-
*
|
|
190
|
-
* normalization, so case and a redundant `:80` never decide trust.
|
|
192
|
+
* hostname on any port. Both sides compare through WHATWG normalization,
|
|
193
|
+
* so case and a redundant scheme-default port never decide trust.
|
|
191
194
|
*/
|
|
192
|
-
function isTrustedAuthority(hostUrl, trustedHosts) {
|
|
195
|
+
function isTrustedAuthority(hostUrl, trustedHosts, protocol) {
|
|
193
196
|
return trustedHosts.some((entry) => {
|
|
194
|
-
const entryUrl = parseAuthority(entry);
|
|
197
|
+
const entryUrl = parseAuthority(entry, protocol);
|
|
195
198
|
if (entryUrl === void 0) return false;
|
|
196
199
|
return canonicalAuthority(entry, entryUrl) === entryUrl.hostname ? entryUrl.hostname === hostUrl.hostname : entryUrl.host === hostUrl.host;
|
|
197
200
|
});
|
|
198
201
|
}
|
|
199
202
|
/**
|
|
203
|
+
* Classify an authority already accepted by {@link assertTrustedAuthority}.
|
|
204
|
+
* @param entry - the configured authority, verbatim.
|
|
205
|
+
* @returns true unless the entry parses as a loopback hostname.
|
|
206
|
+
*/
|
|
207
|
+
function isRemoteAuthority(entry) {
|
|
208
|
+
const url = parseAuthority(entry);
|
|
209
|
+
return url === void 0 || !isLoopbackHostname(url.hostname);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Whether a parsed authority names the deployment's own bind address. A browser
|
|
213
|
+
* dialing that literal sends it as Host, and accepting it grants nothing
|
|
214
|
+
* further: the literal is no rebinding target, the Origin and cross-site checks
|
|
215
|
+
* still apply, and it stays outside `trustedHosts`. Matching ignores the port,
|
|
216
|
+
* like a port-less entry, and any `%zone` id (an interface selector, not
|
|
217
|
+
* address text).
|
|
218
|
+
*/
|
|
219
|
+
function isBindAddressAuthority(hostUrl, bindHost) {
|
|
220
|
+
if (bindHost === void 0) return false;
|
|
221
|
+
const zoneAt = bindHost.indexOf("%");
|
|
222
|
+
const address = zoneAt === -1 ? bindHost : bindHost.slice(0, zoneAt);
|
|
223
|
+
const bindUrl = parseAuthority(address.includes(":") ? `[${address}]` : address);
|
|
224
|
+
return bindUrl !== void 0 && bindUrl.hostname === hostUrl.hostname;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
200
227
|
* Decide whether one /api request may reach the RPC bridge.
|
|
201
228
|
* @param request - Node HTTP or Fetch request facts (headers).
|
|
202
229
|
* @param trustedHosts - non-loopback authorities this deployment serves: exact `host:port`, or port-less `host` matching any port.
|
|
203
|
-
* @
|
|
230
|
+
* @param bindHost - the listener's own bind IP literal, accepted on any port independently of `trustedHosts`.
|
|
231
|
+
* @param protocol - listener protocol for default Host ports; HTTP when no Web carrier is mounted.
|
|
232
|
+
* @returns true when the Host is ours (loopback, the bind address, or trusted) and any attached browser markers are same-origin.
|
|
204
233
|
*/
|
|
205
|
-
function isTrustedApiRequest(request, trustedHosts) {
|
|
234
|
+
function isTrustedApiRequest(request, trustedHosts, bindHost, protocol = "http:") {
|
|
206
235
|
const host = header$1(request.headers, "host");
|
|
207
236
|
if (host === void 0) return false;
|
|
208
|
-
const hostUrl = parseAuthority(host);
|
|
237
|
+
const hostUrl = parseAuthority(host, protocol);
|
|
209
238
|
if (hostUrl === void 0) return false;
|
|
210
|
-
if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts)) return false;
|
|
239
|
+
if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts, protocol) && !isBindAddressAuthority(hostUrl, bindHost)) return false;
|
|
211
240
|
if (header$1(request.headers, "sec-fetch-site") === "cross-site") return false;
|
|
212
241
|
const origin = header$1(request.headers, "origin");
|
|
213
242
|
if (origin === void 0) return true;
|
|
243
|
+
return isSameAuthorityOrigin(origin, host);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Compare Host under the Origin's HTTP(S) scheme so default ports normalize
|
|
247
|
+
* consistently. The transport may remain HTTP behind an HTTPS proxy.
|
|
248
|
+
* @param origin - verbatim Origin header value.
|
|
249
|
+
* @param host - verbatim Host header value already accepted by the Host fence.
|
|
250
|
+
* @returns true only when the Origin is this listener's own `http(s)` authority.
|
|
251
|
+
*/
|
|
252
|
+
function isSameAuthorityOrigin(origin, host) {
|
|
253
|
+
let originUrl;
|
|
214
254
|
try {
|
|
215
|
-
|
|
255
|
+
originUrl = new URL(origin);
|
|
216
256
|
} catch {
|
|
217
257
|
return false;
|
|
218
258
|
}
|
|
259
|
+
const scheme = originUrl.protocol === "https:" ? "https:" : originUrl.protocol === "http:" ? "http:" : void 0;
|
|
260
|
+
if (scheme === void 0) return false;
|
|
261
|
+
const hostUrl = parseAuthority(host, scheme);
|
|
262
|
+
return hostUrl !== void 0 && originUrl.host === hostUrl.host;
|
|
219
263
|
}
|
|
220
264
|
//#endregion
|
|
221
265
|
//#region lib/types/browser-auth.js
|
|
@@ -253,12 +297,13 @@ function header(headers, name) {
|
|
|
253
297
|
const value = headers[name];
|
|
254
298
|
return typeof value === "string" ? value : void 0;
|
|
255
299
|
}
|
|
256
|
-
/**
|
|
257
|
-
function
|
|
300
|
+
/** Protocol-scoped cookie audience; HTTP retains its existing authority-only spelling. */
|
|
301
|
+
function requestAudience(headers, secure) {
|
|
258
302
|
const host = header(headers, "host");
|
|
259
303
|
if (host === void 0) return void 0;
|
|
260
304
|
try {
|
|
261
|
-
|
|
305
|
+
const url = new URL(`${secure ? "https:" : "http:"}//${host}`);
|
|
306
|
+
return secure ? url.origin : url.host;
|
|
262
307
|
} catch {
|
|
263
308
|
return;
|
|
264
309
|
}
|
|
@@ -293,8 +338,9 @@ function cookieValue(headerValue, name) {
|
|
|
293
338
|
}
|
|
294
339
|
}
|
|
295
340
|
/** Serialize the fixed browser-session attributes; generated names and values are cookie-safe base64url. */
|
|
296
|
-
function sessionCookie(name, value, expiresAt, maxAgeSeconds) {
|
|
297
|
-
|
|
341
|
+
function sessionCookie(name, value, expiresAt, maxAgeSeconds, secure) {
|
|
342
|
+
const attributes = `${name}=${value}; Max-Age=${String(maxAgeSeconds)}; Path=/; Expires=${new Date(expiresAt).toUTCString()}; HttpOnly; SameSite=Strict`;
|
|
343
|
+
return secure ? `${attributes}; Secure` : attributes;
|
|
298
344
|
}
|
|
299
345
|
function signature(secret, body) {
|
|
300
346
|
return createHmac("sha256", secret).update(body).digest();
|
|
@@ -383,14 +429,18 @@ var BrowserAuth = class BrowserAuth {
|
|
|
383
429
|
* 401 response.
|
|
384
430
|
* @param req - incoming root or configured-index request.
|
|
385
431
|
* @param res - response owned when this method returns false.
|
|
432
|
+
* @param secure - whether the listener receiving this request serves TLS; the
|
|
433
|
+
* minted cookie carries `Secure` when it does. Only that listener's own
|
|
434
|
+
* protocol decides, never a `publicUrl` announcement or a forwarded header:
|
|
435
|
+
* those describe a hop this process does not terminate.
|
|
386
436
|
* @returns true only when the caller may serve index.html.
|
|
387
437
|
*/
|
|
388
|
-
authorizeIndex(req, res) {
|
|
438
|
+
authorizeIndex(req, res, secure = false) {
|
|
389
439
|
/* v8 ignore next -- node:http always supplies url on server requests. */
|
|
390
440
|
const url = new URL(req.url ?? "/", "http://dsh.invalid");
|
|
391
441
|
const tokens = url.searchParams.getAll(TOKEN_QUERY);
|
|
392
442
|
if (tokens.length > 0) {
|
|
393
|
-
const authority =
|
|
443
|
+
const authority = requestAudience(req.headers, secure);
|
|
394
444
|
if (req.method === "GET" && url.pathname === "/" && tokens.length === 1 && authority !== void 0 && tokenMatches(tokens.join(""), this.launchToken)) {
|
|
395
445
|
const issuedAt = Date.now();
|
|
396
446
|
const expiresAt = issuedAt + this.maxAgeMilliseconds;
|
|
@@ -404,12 +454,12 @@ var BrowserAuth = class BrowserAuth {
|
|
|
404
454
|
"cache-control": "no-store",
|
|
405
455
|
"location": "./",
|
|
406
456
|
"referrer-policy": "no-referrer",
|
|
407
|
-
"set-cookie": sessionCookie(cookieName(authority), value, expiresAt, Math.floor(this.maxAgeMilliseconds / 1e3))
|
|
457
|
+
"set-cookie": sessionCookie(cookieName(authority), value, expiresAt, Math.floor(this.maxAgeMilliseconds / 1e3), secure)
|
|
408
458
|
});
|
|
409
459
|
res.end();
|
|
410
460
|
return false;
|
|
411
461
|
}
|
|
412
|
-
if (req.method === "GET" && url.pathname === "/" && this.isAuthenticated(req)) {
|
|
462
|
+
if (req.method === "GET" && url.pathname === "/" && this.isAuthenticated(req, secure)) {
|
|
413
463
|
res.writeHead(303, {
|
|
414
464
|
"cache-control": "no-store",
|
|
415
465
|
"location": "./",
|
|
@@ -421,17 +471,18 @@ var BrowserAuth = class BrowserAuth {
|
|
|
421
471
|
this.writeUnauthorized(req, res);
|
|
422
472
|
return false;
|
|
423
473
|
}
|
|
424
|
-
if (this.isAuthenticated(req)) return true;
|
|
474
|
+
if (this.isAuthenticated(req, secure)) return true;
|
|
425
475
|
this.writeUnauthorized(req, res);
|
|
426
476
|
return false;
|
|
427
477
|
}
|
|
428
478
|
/**
|
|
429
479
|
* Verify the authority-bound browser cookie on a Host request.
|
|
430
480
|
* @param request - request headers carrying Host and Cookie.
|
|
481
|
+
* @param secure - whether the receiving listener uses TLS, including its default port.
|
|
431
482
|
* @returns true only for an unexpired cookie signed by this activation's loaded secret.
|
|
432
483
|
*/
|
|
433
|
-
isAuthenticated(request) {
|
|
434
|
-
const authority =
|
|
484
|
+
isAuthenticated(request, secure = false) {
|
|
485
|
+
const authority = requestAudience(request.headers, secure);
|
|
435
486
|
const rawCookie = header(request.headers, "cookie");
|
|
436
487
|
if (authority === void 0 || rawCookie === void 0) return false;
|
|
437
488
|
const value = cookieValue(rawCookie, cookieName(authority));
|
|
@@ -556,6 +607,7 @@ var HostConnectionService = class extends Service {
|
|
|
556
607
|
operator;
|
|
557
608
|
interceptors = /* @__PURE__ */ new Map();
|
|
558
609
|
fetchRoutes = /* @__PURE__ */ new Map();
|
|
610
|
+
allowsRemoteAuthorities;
|
|
559
611
|
/**
|
|
560
612
|
* Provide the Host half over the active HTTP server.
|
|
561
613
|
* @param ctx - owning Connection plugin context.
|
|
@@ -568,6 +620,7 @@ var HostConnectionService = class extends Service {
|
|
|
568
620
|
this.browserAuth = browserAuth;
|
|
569
621
|
this.operator = new OperatorPeer(ctx);
|
|
570
622
|
ctx.effect(() => () => this.operator.dispose(), "client-connection: operator Peer");
|
|
623
|
+
this.allowsRemoteAuthorities = trustedHosts.some(isRemoteAuthority);
|
|
571
624
|
}
|
|
572
625
|
/** Generic channel registry scoped to the Context reading this service. */
|
|
573
626
|
get rpc() {
|
|
@@ -582,19 +635,22 @@ var HostConnectionService = class extends Service {
|
|
|
582
635
|
const owner = this.ctx;
|
|
583
636
|
return { register: (route) => this.registerFetchRoute(owner, route) };
|
|
584
637
|
}
|
|
585
|
-
/** Apply the
|
|
638
|
+
/** Apply the Host/Origin fence, then browser authentication. */
|
|
586
639
|
requestRejection(request) {
|
|
587
|
-
|
|
588
|
-
|
|
640
|
+
const carrier = this.ctx.get("webServer");
|
|
641
|
+
if (!isTrustedApiRequest(request, this.trustedHosts, carrier?.host, carrier?.protocol ?? "http:")) return 403;
|
|
642
|
+
return this.browserAuth.isAuthenticated(request, carrier?.protocol === "https:") ? void 0 : 401;
|
|
589
643
|
}
|
|
590
644
|
/** A request that passes the fence and authentication speaks for the operator. */
|
|
591
645
|
admit(request) {
|
|
592
646
|
const rejection = this.requestRejection(request);
|
|
593
647
|
return rejection === void 0 ? { peer: this.operator } : { rejection };
|
|
594
648
|
}
|
|
595
|
-
/**
|
|
649
|
+
/**
|
|
650
|
+
* Authenticate an index request; native TLS listeners mint Secure cookies.
|
|
651
|
+
*/
|
|
596
652
|
authorizeIndex(request, response) {
|
|
597
|
-
return this.browserAuth.authorizeIndex(request, response);
|
|
653
|
+
return this.browserAuth.authorizeIndex(request, response, this.ctx.get("webServer")?.protocol === "https:");
|
|
598
654
|
}
|
|
599
655
|
/** Add this process's launch token to the clean application URL. */
|
|
600
656
|
authenticatedUrl(baseUrl) {
|
|
@@ -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
|
|
@@ -33,15 +33,20 @@ export declare class BrowserAuth {
|
|
|
33
33
|
* 401 response.
|
|
34
34
|
* @param req - incoming root or configured-index request.
|
|
35
35
|
* @param res - response owned when this method returns false.
|
|
36
|
+
* @param secure - whether the listener receiving this request serves TLS; the
|
|
37
|
+
* minted cookie carries `Secure` when it does. Only that listener's own
|
|
38
|
+
* protocol decides, never a `publicUrl` announcement or a forwarded header:
|
|
39
|
+
* those describe a hop this process does not terminate.
|
|
36
40
|
* @returns true only when the caller may serve index.html.
|
|
37
41
|
*/
|
|
38
|
-
authorizeIndex(req: ConnectionIndexRequest, res: ConnectionIndexResponse): boolean;
|
|
42
|
+
authorizeIndex(req: ConnectionIndexRequest, res: ConnectionIndexResponse, secure?: boolean): boolean;
|
|
39
43
|
/**
|
|
40
44
|
* Verify the authority-bound browser cookie on a Host request.
|
|
41
45
|
* @param request - request headers carrying Host and Cookie.
|
|
46
|
+
* @param secure - whether the receiving listener uses TLS, including its default port.
|
|
42
47
|
* @returns true only for an unexpired cookie signed by this activation's loaded secret.
|
|
43
48
|
*/
|
|
44
|
-
isAuthenticated(request: ConnectionTrustRequest): boolean;
|
|
49
|
+
isAuthenticated(request: ConnectionTrustRequest, secure?: boolean): boolean;
|
|
45
50
|
private writeUnauthorized;
|
|
46
51
|
}
|
|
47
52
|
//# sourceMappingURL=browser-auth.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -33,11 +33,9 @@ export interface ConnectionConfig {
|
|
|
33
33
|
recovery?: ConnectionRecoveryConfig;
|
|
34
34
|
/**
|
|
35
35
|
* Authorities this deployment serves beyond loopback: exact `host:port`, or
|
|
36
|
-
* port-less `host` matching any port. The
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* by; the Web runtime derives LAN IP literals from an active all-interface
|
|
40
|
-
* bind. An entry that is not a bare, canonical authority fails plugin load.
|
|
36
|
+
* port-less `host` matching any port. The fence accepts loopback and the
|
|
37
|
+
* listener's own bind IP independently; other remote authorities require
|
|
38
|
+
* an entry here. A non-canonical or non-bare authority fails plugin load.
|
|
41
39
|
*/
|
|
42
40
|
trustedHosts?: string[];
|
|
43
41
|
/** Absolute browser-session lifetime in days. Default: 30. */
|
package/lib/types/rpc-host.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class HostConnectionService extends Service implements HostConnec
|
|
|
17
17
|
readonly operator: PeerScope;
|
|
18
18
|
private readonly interceptors;
|
|
19
19
|
private readonly fetchRoutes;
|
|
20
|
+
readonly allowsRemoteAuthorities: boolean;
|
|
20
21
|
/**
|
|
21
22
|
* Provide the Host half over the active HTTP server.
|
|
22
23
|
* @param ctx - owning Connection plugin context.
|
|
@@ -28,11 +29,13 @@ export declare class HostConnectionService extends Service implements HostConnec
|
|
|
28
29
|
get rpc(): HostConnectionRpc;
|
|
29
30
|
/** Exact Fetch-route registry scoped to the Context reading this service. */
|
|
30
31
|
get fetch(): HostConnectionFetch;
|
|
31
|
-
/** Apply the
|
|
32
|
+
/** Apply the Host/Origin fence, then browser authentication. */
|
|
32
33
|
requestRejection(request: ConnectionTrustRequest): ConnectionRequestRejection;
|
|
33
34
|
/** A request that passes the fence and authentication speaks for the operator. */
|
|
34
35
|
admit(request: ConnectionTrustRequest): PeerAdmission;
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Authenticate an index request; native TLS listeners mint Secure cookies.
|
|
38
|
+
*/
|
|
36
39
|
authorizeIndex(request: ConnectionIndexRequest, response: ConnectionIndexResponse): boolean;
|
|
37
40
|
/** Add this process's launch token to the clean application URL. */
|
|
38
41
|
authenticatedUrl(baseUrl: string): string;
|
package/lib/types/rpc.d.ts
CHANGED
|
@@ -153,6 +153,11 @@ export interface HostConnectionHandle {
|
|
|
153
153
|
readonly fetch: HostConnectionFetch;
|
|
154
154
|
/** The operator Peer every admitted request speaks for; its scope lives as long as Connection. */
|
|
155
155
|
readonly operator: PeerScope;
|
|
156
|
+
/**
|
|
157
|
+
* Whether this deployment accepts authorities beyond loopback: true only
|
|
158
|
+
* when a validated `trustedHosts` entry names a non-loopback hostname.
|
|
159
|
+
*/
|
|
160
|
+
readonly allowsRemoteAuthorities: boolean;
|
|
156
161
|
/**
|
|
157
162
|
* Compose exact Fetch routes and the shared-channel RPC interceptor.
|
|
158
163
|
* @param channel - shared channel mounted by Connection.
|
|
@@ -175,6 +180,8 @@ export interface HostConnectionHandle {
|
|
|
175
180
|
admit(request: ConnectionTrustRequest): PeerAdmission;
|
|
176
181
|
/**
|
|
177
182
|
* Authenticate one frontend index request, owning a token redirect or 401.
|
|
183
|
+
* The cookie this mints is `Secure` when the mounted Web carrier serves TLS,
|
|
184
|
+
* which only that listener's protocol decides.
|
|
178
185
|
* @param request - root or configured-index HTTP request.
|
|
179
186
|
* @param response - response owned when the result is false.
|
|
180
187
|
* @returns true only when the frontend may serve index.html.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-connection",
|
|
3
3
|
"description": "Authenticated RPC transport and generation lifecycle",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
},
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"@deepseek-ai/
|
|
39
|
-
"
|
|
37
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-alpha.2",
|
|
38
|
+
"@deepseek-ai/schemastery": "~3.18.5-alpha.1",
|
|
39
|
+
"zod": "^4.4.3"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"lib/index.js",
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"lib/types/**/*.d.ts"
|
|
45
45
|
],
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@deepseek-ai/
|
|
48
|
-
"@deepseek-ai/
|
|
47
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "~4.0.
|
|
52
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
53
|
-
"@deepseek-ai/dsh-brand": "0.2.
|
|
54
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.
|
|
55
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
56
|
-
"@deepseek-ai/dsh-scope": "0.2.
|
|
57
|
-
"@deepseek-ai/dsh-session": "0.2.
|
|
58
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.
|
|
51
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2"
|
|
59
59
|
}
|
|
60
60
|
}
|