@jayyuen66/dsh-plugin-shared 0.1.2 → 0.1.4
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 +10 -8
- package/{README.zh.md → README.zh-CN.md} +10 -8
- package/dist/index.js +1 -1
- package/dist/trust.js +1 -1
- package/dist/types/trust.d.ts +22 -1
- package/package.json +48 -24
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jayyuen66/dsh-plugin-shared
|
|
2
2
|
|
|
3
|
-
[English](./README.md) · [简体中文](./README.zh.md)
|
|
3
|
+
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
5
|
## What this package is
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ End users do not install or enable it separately: any consumer package pulls it
|
|
|
33
33
|
| | |
|
|
34
34
|
| --- | --- |
|
|
35
35
|
| Node.js | `^22.19.0 \|\| >=24.0.0`, the same clause as the harness root |
|
|
36
|
-
| dsh | `>= 0.2.1-alpha.
|
|
36
|
+
| dsh | `>= 0.2.1-alpha.2`, declared as an **optional** peer (this package needs no host at import time) |
|
|
37
37
|
| Module format | ESM only. `require()` works only where Node supports `require(esm)` |
|
|
38
38
|
| Tree shaking | `"sideEffects": false` |
|
|
39
39
|
|
|
@@ -64,7 +64,7 @@ The subpath column is the in-package specifier; value imports must prefix it wit
|
|
|
64
64
|
| `./lib/record` | `isRecord`, `fieldOf` |
|
|
65
65
|
| `./lib/errors` | `errorText`: semantics aligned with the official `@deepseek-ai/dsh-llm` `errorChain` (cause chain, `AggregateError` members, empty message falling back to `name`, cross-realm `message` read off the value itself, hostile getters degraded), implemented locally with zero dependencies |
|
|
66
66
|
| `./lib/jsonl` | `shrinkJsonlTail`: the pure decision half of JSONL tail halving; trigger policy and error exit stay with the caller |
|
|
67
|
-
| `./lib/trust` | `requestTrust` / `guardTrust` / `trustRejectionText`: request-trust predicate for `/_dsh/*` endpoints (Host authority → `sec-fetch-site` allow-list → byte-exact Origin) |
|
|
67
|
+
| `./lib/trust` | `requestTrust` / `guardTrust` / `trustRejectionText`: request-trust predicate for `/_dsh/*` endpoints (Host authority → `sec-fetch-site` allow-list → byte-exact Origin), plus `isNonLoopbackBindHost` to derive `servingNonLoopback` from the `webServer.host` bind literal |
|
|
68
68
|
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome`: maps a settled subprocess onto the official job registry's outcome |
|
|
69
69
|
| `./lib/canonicalize-region-paths` | Build-time string helper that folds rolldown's `//#region <path>` markers into a `process.cwd()`-independent form. Exported as a subpath precisely because runtime consumers should not pay an import for it |
|
|
70
70
|
|
|
@@ -97,11 +97,12 @@ The sonarjs entry point is resolved lazily, inside `definePluginConfig()`. Impor
|
|
|
97
97
|
|
|
98
98
|
## Dependencies
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
`dependencies` is empty; the seven `@deepseek-ai/*` packages appear only in `peerDependencies` and `devDependencies`. `test/publish-manifest.test.ts` enforces this — it is not a style preference:
|
|
101
101
|
|
|
102
|
-
- `@deepseek-ai/dsh-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
102
|
+
- **Putting them in `dependencies` disables the consumer's host.** Consumers are dsh plugins installed into a profile (`nodeLinker: hoisted`, `autoInstallPeers: false`). Any host package listed under `dependencies` is written into the profile as a **real directory**, coexisting with the copy the host process already loaded. `@deepseek-ai/dsh-tools` keys its `TOOL_RUNTIME_SCHEDULER` with a plain `Symbol` (not `Symbol.for`), so symbol identity is per module instance: the host's `dsh-agent-loop` reads a `ToolRuntime` mounted from the duplicate using its own copy's symbol, gets `undefined`, and **every native tool call throws** `Cannot read properties of undefined (reading 'prepare')` — the whole profile loses its tools. This package's build, typecheck and tests all stay green right up to that point, so the invariant has to live in the manifest.
|
|
103
|
+
- `@deepseek-ai/dsh-output-retention` is a **value** import (`truncateEnd`) and is provided by the host at runtime, so it is a required peer.
|
|
104
|
+
- The other six are referenced only by the published `.d.ts`, so they are optional peers plus devDependencies: the peer keeps the type surface honest — `lib/job-outcome` used to take `ShellProcess` and `JobOutcome` from packages no consumer tree was required to contain, so with `skipLibCheck: true` a wrong `status` or `exitCode` produced no error at all and `jobOutcomeOf`'s return type collapsed to `any` — while devDependencies let this repository compile and test. Profiles never install peers, so no duplicate appears.
|
|
105
|
+
- Versions follow the host: exact pins throughout the `dsh-*` family and for `cordis`, matching what the host bundle itself declares so the same copy is reused rather than duplicated.
|
|
105
106
|
|
|
106
107
|
Known upstream limitation: `@deepseek-ai/dsh-llm`'s own declarations import `@deepseek-ai/dsh-attachment`, which it does not declare. It is reached transitively through `dsh-session`, so a consumer running with `skipLibCheck: false` sees `TS2307` from *that* package, not from this one.
|
|
107
108
|
|
|
@@ -110,7 +111,7 @@ Known upstream limitation: `@deepseek-ai/dsh-llm`'s own declarations import `@de
|
|
|
110
111
|
`main` and `exports` point at `dist/` only. There is no second, source-shaped manifest.
|
|
111
112
|
|
|
112
113
|
- `publishConfig` carries `access` and `registry` and nothing else. Putting `main`/`exports` there was wrong: pnpm merges those onto the top level when publishing and npm does not, so publishing with npm shipped a package whose fourteen entry points named files that were not in the tarball.
|
|
113
|
-
- `files` is `icon.svg`, `dist`, `config/*.json`, `README.zh.md`. npm always adds `package.json`, `LICENSE` and `README.md`; `README.zh.md` is listed explicitly so the Chinese copy ships too.
|
|
114
|
+
- `files` is `icon.svg`, `dist`, `config/*.json`, `README.zh-CN.md`. npm always adds `package.json`, `LICENSE` and `README.md`; `README.zh-CN.md` is listed explicitly so the Chinese copy ships too.
|
|
114
115
|
- `build` starts by deleting `dist/`, because `files` takes the whole directory and a renamed facet would otherwise keep publishing its orphan artifact.
|
|
115
116
|
- `prepare` runs `build`. It does not run when a consumer installs from the registry; it exists so packing, publishing and workspace links always ship a fresh `dist/`.
|
|
116
117
|
- Node throws `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` for `.ts` inside `node_modules`, which is exactly where a published package is loaded from, so an artifact is mandatory. The barrel's declaration file is generated by `build-host.mjs` because source-form `export * from "./http.ts"` specifiers are not resolvable by consumers.
|
|
@@ -126,6 +127,7 @@ Known upstream limitation: `@deepseek-ai/dsh-llm`'s own declarations import `@de
|
|
|
126
127
|
- **`errorText` now returns more text** (same name and signature). It went from a single `message` layer to the official `errorChain` semantics: a cause chain renders as `outer: inner`, an `AggregateError` appends `[e1; e2]`, an empty message falls back to `Error.name`, and a cross-realm `Error` yields its own `message` instead of `String()`'s `"Error: ..."`. **Callers that regex the old text must re-check** (a `exec` that pulls a number out of parentheses, for example).
|
|
127
128
|
- `editPathOf` returns a `kind`-discriminated `EditTarget` with only `write` / `read-view`: `"skip"` was never produced, and leaving it in the type only pushed consumers into writing unreachable branches. When no path can be read it still returns the matching arm with `path` set to `undefined`.
|
|
128
129
|
- `isCrossOrigin` / `queryParam` / `checkCsrf` / `requestTrust` / `guardTrust` widened their first parameter from `IncomingMessage` to `HttpRequest`. `readBody` / `guardBody` still take `IncomingMessage` because they consume the request body and `PartialRequest` does not guarantee async iteration.
|
|
130
|
+
- **`servingNonLoopback` must now come from `isNonLoopbackBindHost(webServer.host)`, not from `webServer.host === "0.0.0.0"`.** Host 0.2.1-alpha.2 changed `@deepseek-ai/dsh-host-webserver`'s `Config.host` from the two-value union `'127.0.0.1' | '0.0.0.0'` to any concrete local-interface IP literal and started rejecting wildcard bind addresses at load. The old comparison is therefore constant `false`: a plugin bound to a real interface address stops recognizing its own authority, and every LAN request comes back `403 untrusted host authority`. `TrustOptions` is unchanged, so this is not an API break — it is a fix for a derivation that silently stopped working. `test/trust.test.ts` cross-checks the new predicate against the official `isLoopbackHost`, which became a public export in the same host release.
|
|
129
131
|
|
|
130
132
|
## Quality gates
|
|
131
133
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jayyuen66/dsh-plugin-shared
|
|
2
2
|
|
|
3
|
-
[English](./README.md) · [简体中文](./README.zh.md)
|
|
3
|
+
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
5
|
## 这个包是什么
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ import { readBody, sendJson } from "@jayyuen66/dsh-plugin-shared/lib/http";
|
|
|
33
33
|
| | |
|
|
34
34
|
| --- | --- |
|
|
35
35
|
| Node.js | `^22.19.0 \|\| >=24.0.0`,与 harness 根同款 |
|
|
36
|
-
| dsh | `>= 0.2.1-alpha.
|
|
36
|
+
| dsh | `>= 0.2.1-alpha.2`,声明为 **optional** peer(本包 import 时不需要宿主在场) |
|
|
37
37
|
| 模块形态 | 仅 ESM。`require()` 只在支持 `require(esm)` 的 Node 上可用 |
|
|
38
38
|
| Tree shaking | `"sideEffects": false` |
|
|
39
39
|
|
|
@@ -64,7 +64,7 @@ import { readBody, sendJson } from "@jayyuen66/dsh-plugin-shared/lib/http";
|
|
|
64
64
|
| `./lib/record` | `isRecord`、`fieldOf` |
|
|
65
65
|
| `./lib/errors` | `errorText`:语义对齐官方 `@deepseek-ai/dsh-llm` 的 `errorChain`(cause 链、`AggregateError` 成员、空 message 回退 name、跨 realm 取自有 message、敌意 getter 降级),本地零依赖实现 |
|
|
66
66
|
| `./lib/jsonl` | `shrinkJsonlTail`:JSONL 尾部收缩的纯决策件;触发策略与错误出口留给调用方 |
|
|
67
|
-
| `./lib/trust` | `requestTrust` / `guardTrust` / `trustRejectionText`:`/_dsh/*` 端点的请求信任判据(Host 权威 → `sec-fetch-site` 白名单 → Origin
|
|
67
|
+
| `./lib/trust` | `requestTrust` / `guardTrust` / `trustRejectionText`:`/_dsh/*` 端点的请求信任判据(Host 权威 → `sec-fetch-site` 白名单 → Origin 逐字比对);`isNonLoopbackBindHost`:由 `webServer.host` 那枚绑定字面量取 `servingNonLoopback` |
|
|
68
68
|
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome`:把落定的子进程映射成官方作业注册表的结局 |
|
|
69
69
|
| `./lib/canonicalize-region-paths` | 构建期字符串件:把 rolldown 写的 `//#region <路径>` 折成与 `process.cwd()` 无关的形态。之所以只给子路径,就是为了让运行期消费方不必为它付一次导入 |
|
|
70
70
|
|
|
@@ -97,11 +97,12 @@ sonarjs 的入口是**惰性解析**的,发生在 `definePluginConfig()` 内
|
|
|
97
97
|
|
|
98
98
|
## 依赖
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
`dependencies` 是空的;七枚 `@deepseek-ai/*` 只出现在 `peerDependencies` 与 `devDependencies` 里。这条由 `test/publish-manifest.test.ts` 钉住,不是风格偏好:
|
|
101
101
|
|
|
102
|
-
- `@deepseek-ai/dsh-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
102
|
+
- **写进 `dependencies` 会让消费方的宿主瘫掉。** 消费方是 dsh 插件,被装进 profile(`nodeLinker: hoisted`、`autoInstallPeers: false`)。宿主包只要出现在 `dependencies`,pnpm 就在 profile 里把它们落成**真实目录**,与宿主进程里已有的那份并存。而 `@deepseek-ai/dsh-tools` 的 `TOOL_RUNTIME_SCHEDULER` 是普通 `Symbol`(不是 `Symbol.for`),符号身份按模块实例计算——宿主 `dsh-agent-loop` 拿自己那份的 Symbol 去读由副本挂载出来的 `ToolRuntime`,得到 `undefined`,于是**每一次原生工具调用都抛** `Cannot read properties of undefined (reading 'prepare')`,整个 profile 的工具一起失效。本包的构建、类型检查与测试在这之前全部照绿,所以判据只能落在 manifest 面。
|
|
103
|
+
- `@deepseek-ai/dsh-output-retention` 是**值导入**(`truncateEnd` 用它),运行期由宿主提供,因此是非 optional 的 peer。
|
|
104
|
+
- 其余六枚只被发布出去的 `.d.ts` 引用,声明成 optional peer 加 dev:peer 保住类型面不静默降级——`lib/job-outcome` 的 `ShellProcess`/`JobOutcome` 曾经来自消费方根本不保证存在的包,于是在 `skipLibCheck: true` 下写错 `status` 或 `exitCode` 一条都不报,返回值类型直接塌成 `any`;dev 让本包自己的编译与测试解析得到。profile 不装 peer,所以不产生副本。
|
|
105
|
+
- 版本口径跟着宿主走:`dsh-*` 家族内一律精确钉,`cordis` 也精确钉,与宿主本体自己声明的一致,这样是复用同一份而不是装出第二份。
|
|
105
106
|
|
|
106
107
|
已知的上游限制:`@deepseek-ai/dsh-llm` 的声明文件引用了它自己没声明的 `@deepseek-ai/dsh-attachment`。它经 `dsh-session` 传递进来,所以开 `skipLibCheck: false` 的消费方会看到来自**那个包**的 `TS2307`,不是来自本包。
|
|
107
108
|
|
|
@@ -110,7 +111,7 @@ sonarjs 的入口是**惰性解析**的,发生在 `definePluginConfig()` 内
|
|
|
110
111
|
`main` 与 `exports` 只指 `dist/`,不存在第二套源码形态的 manifest。
|
|
111
112
|
|
|
112
113
|
- `publishConfig` 只带 `access` 和 `registry`。把 `main`/`exports` 放在那里是错的:pnpm 发布时会把它们合并到顶层,npm 不会——用 npm 发出去的包,十四个入口指向的都是 tarball 里不存在的文件。
|
|
113
|
-
- `files` 是 `icon.svg`、`dist`、`config/*.json`、`README.zh.md`。npm 永远附带 `package.json`、`LICENSE` 和 `README.md`;`README.zh.md` 显式列出才会一起发。
|
|
114
|
+
- `files` 是 `icon.svg`、`dist`、`config/*.json`、`README.zh-CN.md`。npm 永远附带 `package.json`、`LICENSE` 和 `README.md`;`README.zh-CN.md` 显式列出才会一起发。
|
|
114
115
|
- `build` 第一步删除 `dist/`:`files` 整目录收,切面一旦改名,上一代孤儿产物会继续被发出去。
|
|
115
116
|
- `prepare` 挂 `build`。消费方从 registry 安装时它不执行;它存在是为了让打包、发布与 workspace link 拿到的都是新鲜 `dist/`。
|
|
116
117
|
- 必须有编译产物:Node 对 `node_modules` 里的 `.ts` 直接抛 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,而发出去的包正是躺在那里被载入的。barrel 的声明文件由 `build-host.mjs` 生成,因为源码形态的 `export * from "./http.ts"` 消费方解析不到。
|
|
@@ -126,6 +127,7 @@ sonarjs 的入口是**惰性解析**的,发生在 `definePluginConfig()` 内
|
|
|
126
127
|
- **`errorText` 的返回值变长了**(签名与名字不变)。它从"单层 message"扩成与官方 `errorChain` 同语义:cause 链拼成 `外: 内`、`AggregateError` 附 `[e1; e2]`、空 message 回退 `Error.name`、跨 realm 的 Error 取其自带 `message` 而不是 `String()` 的 `"Error: …"`。**依赖旧文本做正则匹配的调用方要重新核对**(例如按括号取数的那类 `exec`)。
|
|
127
128
|
- `editPathOf` 的返回类型收成按 `kind` 判别的 `EditTarget`,只保留 `write` / `read-view` 两支——`"skip"` 从来不会被产出,留在类型里只会逼调用方写永不执行的分支。路径取不到时仍返回所属那一支、`path` 为 `undefined`。
|
|
128
129
|
- `isCrossOrigin` / `queryParam` / `checkCsrf` / `requestTrust` / `guardTrust` 的首参数由 `IncomingMessage` **放宽**为 `HttpRequest`。`readBody` / `guardBody` 仍是 `IncomingMessage`:它们要消费请求体,而 `PartialRequest` 不保证可异步迭代。
|
|
130
|
+
- **`servingNonLoopback` 现在要由 `isNonLoopbackBindHost(webServer.host)` 取,别再写 `webServer.host === "0.0.0.0"`。** 宿主 0.2.1-alpha.2 把 `@deepseek-ai/dsh-host-webserver` 的 `Config.host` 从 `'127.0.0.1' | '0.0.0.0'` 换成任意本机接口的 IP 字面量,并在载入时拒掉通配地址。于是那句旧比较恒为 false:绑在真实网卡地址上的插件认不出自己的权威,局域网来的请求整片吃 `403 untrusted host authority`。`TrustOptions` 的签名没动,所以这不是 API 破坏,而是一条**悄悄失效**的取值口径的修复。`test/trust.test.ts` 会把新谓词与官方 `isLoopbackHost`(同一次宿主发布里才转为公开导出)逐字面对表。
|
|
129
131
|
|
|
130
132
|
## 质量门
|
|
131
133
|
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CROSS_ORIGIN_TEXT as e,checkCsrf as t,guardBody as n,isCrossOrigin as r,queryParam as i,readBody as a,sendJson as o}from"./http.js";import{fieldOf as s,isRecord as c}from"./record.js";import{editPathOf as l,parseToolArguments as u,scanToolEvents as d,toolArgumentsBad as f,toolEventRowsOf as p}from"./tool-events.js";import{claimApply as m}from"./card-apply.js";import{deriveProjectKey as h}from"./project-key.js";import{DEFAULT_LOCALE as g,LOCALE_PREFERENCE_FIELD as _,LOCALE_SETTINGS_NAMESPACE as v,messagesFor as y,resolveLocale as b,resolveLocalePreference as x}from"./locale.js";import{settleLessonCall as S}from"./lesson-bus.js";import{truncateEnd as C,truncateStart as w}from"./text.js";import{errorText as T}from"./errors.js";import{shrinkJsonlTail as E}from"./jsonl.js";import{guardTrust as D,
|
|
1
|
+
import{CROSS_ORIGIN_TEXT as e,checkCsrf as t,guardBody as n,isCrossOrigin as r,queryParam as i,readBody as a,sendJson as o}from"./http.js";import{fieldOf as s,isRecord as c}from"./record.js";import{editPathOf as l,parseToolArguments as u,scanToolEvents as d,toolArgumentsBad as f,toolEventRowsOf as p}from"./tool-events.js";import{claimApply as m}from"./card-apply.js";import{deriveProjectKey as h}from"./project-key.js";import{DEFAULT_LOCALE as g,LOCALE_PREFERENCE_FIELD as _,LOCALE_SETTINGS_NAMESPACE as v,messagesFor as y,resolveLocale as b,resolveLocalePreference as x}from"./locale.js";import{settleLessonCall as S}from"./lesson-bus.js";import{truncateEnd as C,truncateStart as w}from"./text.js";import{errorText as T}from"./errors.js";import{shrinkJsonlTail as E}from"./jsonl.js";import{guardTrust as D,isNonLoopbackBindHost as O,requestTrust as k,trustRejectionText as A}from"./trust.js";import{jobOutcomeOf as j}from"./job-outcome.js";export{e as CROSS_ORIGIN_TEXT,g as DEFAULT_LOCALE,_ as LOCALE_PREFERENCE_FIELD,v as LOCALE_SETTINGS_NAMESPACE,t as checkCsrf,m as claimApply,h as deriveProjectKey,l as editPathOf,T as errorText,s as fieldOf,n as guardBody,D as guardTrust,r as isCrossOrigin,O as isNonLoopbackBindHost,c as isRecord,j as jobOutcomeOf,y as messagesFor,u as parseToolArguments,i as queryParam,a as readBody,k as requestTrust,b as resolveLocale,x as resolveLocalePreference,d as scanToolEvents,o as sendJson,S as settleLessonCall,E as shrinkJsonlTail,f as toolArgumentsBad,p as toolEventRowsOf,C as truncateEnd,w as truncateStart,A as trustRejectionText};
|
package/dist/trust.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CROSS_ORIGIN_TEXT as e,sendJson as t}from"./http.js";import n from"node:os";const
|
|
1
|
+
import{CROSS_ORIGIN_TEXT as e,sendJson as t}from"./http.js";import n from"node:os";import{isIPv4 as r,isIPv6 as i}from"node:net";const a=/* @__PURE__ */ new Set([`localhost`,`[::1]`]),o=/* @__PURE__ */ new Set([`same-origin`,`none`]);function s(e){let t=new URL(`http://[${e.split(`%`,1).join(``)}]`).hostname;return t===`[::1]`||t.startsWith(`[::ffff:7f`)}function c(e){return e===void 0?!1:r(e)?e.split(`.`,1).join(``)!==`127`:i(e)&&!s(e)}const l=`bad-origin`;function u(e,t){let n=e.headers?.[t];return n===void 0?null:Array.isArray(n)?n.length===1?n.join(``):null:n}const d=/^[A-Za-z0-9._\-[\]:%]+$/u;function f(e){if(!d.test(e))return null;try{return new URL(`http://${e}`)}catch{return null}}function p(e){let t=e.toLowerCase();if(a.has(t))return!0;let n=t.split(`.`);return n.length===4&&n[0]===`127`&&n.every(e=>/^\d{1,3}$/u.test(e)&&Number(e)<=255)}function m(){let e=[];for(let t of Object.values(n.networkInterfaces()))for(let{family:n,address:r}of t??[])e.push(n===`IPv6`?`[${r}]`:r);return e}function h(e,t){return p(e)?!0:t.servingNonLoopback===!0&&(t.localInterfaceAddresses??m()).includes(e)}function g(e){let t=u(e,`sec-fetch-site`);return t===null||o.has(t)}function _(e){let t=e.socket?.remoteAddress;if(typeof t!=`string`||t===``)return null;let n=t.replaceAll(/^\[|\]$/gu,``).replace(/^::ffff:/iu,``);return p(n)||n===`::1`}function v(e,t){let n=e.headers?.host;if(Array.isArray(n)&&n.length!==1)return`rejected`;let r=u(e,`host`);if(r===null)return _(e)===!1?`rejected`:null;let i=f(r);return i===null||!h(i.hostname,t)?`rejected`:i}function y(e,t={}){let n=v(e,t);if(n===`rejected`)return`untrusted-host`;if(!g(e))return`cross-site`;let r=u(e,`origin`);if(r===null)return`trusted`;if(r===`null`||n===null)return l;let i=f(r.replace(/^[a-z][a-z0-9+.-]*:\/\//iu,``));return i===null?l:i.host===n.host?`trusted`:l}function b(t){return t===`untrusted-host`?`untrusted host authority`:e}function x(e,n,r={}){let i=y(e,r);return i===`trusted`?!0:n.headersSent||n.writableEnded?(console.error(`[shared/trust] rejecting after headers were sent:`,i),!1):(t(n,403,{ok:!1,error:b(i)}),!1)}export{x as guardTrust,c as isNonLoopbackBindHost,y as requestTrust,b as trustRejectionText};
|
package/dist/types/trust.d.ts
CHANGED
|
@@ -4,11 +4,32 @@ import type { HttpRequest } from "./http.ts";
|
|
|
4
4
|
export type TrustVerdict = "trusted" | "untrusted-host" | "cross-site" | "bad-origin";
|
|
5
5
|
/** `requestTrust` / `guardTrust` 的可选入参。 */
|
|
6
6
|
export interface TrustOptions {
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 宿主是否绑在非回环地址上——由 `isNonLoopbackBindHost(webServer.host)` 取,**不要**再自己比
|
|
9
|
+
* 字面量。缺省按 false(保守)。
|
|
10
|
+
*/
|
|
8
11
|
servingNonLoopback?: boolean;
|
|
9
12
|
/** 注入本机地址面(测试用;缺省取 `os.networkInterfaces()` 的实际值)。 */
|
|
10
13
|
localInterfaceAddresses?: readonly string[];
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* 绑定地址的字面量谓词:`webServer.host` → {@link TrustOptions.servingNonLoopback}。
|
|
17
|
+
*
|
|
18
|
+
* 为什么现在要由本包出这个谓词:`@deepseek-ai/dsh-host-webserver` 的 `Config.host` 在宿主
|
|
19
|
+
* 0.2.1-alpha.2 从 `'127.0.0.1' | '0.0.0.0'` 换成**任意本机接口 IP 字面量**,并把通配地址
|
|
20
|
+
* (`0.0.0.0`、`::` 及其映射形)**在载入时拒掉**。于是消费方沿用至今的那行
|
|
21
|
+
* `webServer.host === "0.0.0.0"` 恒为 false——非回环服务面换了形态(一枚真的网卡地址),
|
|
22
|
+
* 判据却还盯着那枚再也绑不上的字面量,局域网访问会被本模块整片拒成 403。
|
|
23
|
+
*
|
|
24
|
+
* 口径与官方 `isLoopbackHost` 对齐(台架见 `test/trust.test.ts`)。一处**刻意**分歧:非字面量
|
|
25
|
+
* (`localhost`、带方括号的 `[::1]`)本函数按 false,官方那侧 `!isLoopbackHost` 得 true。
|
|
26
|
+
* 宿主自身的 config 校验已经拒掉非字面量,那种值到不了这里;万一到了,false 收紧而非放宽
|
|
27
|
+
* 可放行的 Host 权威——这个方向上宁可错杀。
|
|
28
|
+
*
|
|
29
|
+
* @param host - `webServer.host` 的绑定值;读不到(缺字段、空串、非字面量)按只绑回环处理
|
|
30
|
+
* @returns 绑的是非回环接口地址时为 true
|
|
31
|
+
*/
|
|
32
|
+
export declare function isNonLoopbackBindHost(host: string | undefined): boolean;
|
|
12
33
|
/**
|
|
13
34
|
* 一次判据:Host 权威 → sec-fetch-site(白名单)→ Origin 逐字比对。
|
|
14
35
|
* 纯函数、不写响应;写响应的样板在 `guardTrust`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jayyuen66/dsh-plugin-shared",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "dsh 自研插件的共享工具库:tool 事件流记账扫描 + webServer HTTP 样板(sendJson/CORS/CSRF/body 限制)+ 卡片 apply 骨架 + 项目键派生 + 代理对安全截断。作为库包被 import,不由用户启用",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cordis",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"icon.svg",
|
|
25
25
|
"dist",
|
|
26
26
|
"config/*.json",
|
|
27
|
-
"README.zh.md"
|
|
27
|
+
"README.zh-CN.md"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
30
|
"sideEffects": false,
|
|
@@ -103,41 +103,65 @@
|
|
|
103
103
|
"access": "public",
|
|
104
104
|
"registry": "https://registry.npmjs.org"
|
|
105
105
|
},
|
|
106
|
-
"dependencies": {
|
|
107
|
-
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
108
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.1",
|
|
109
|
-
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.1",
|
|
110
|
-
"@deepseek-ai/dsh-output-retention": "0.2.1-alpha.1",
|
|
111
|
-
"@deepseek-ai/dsh-session": "0.2.1-alpha.1",
|
|
112
|
-
"@deepseek-ai/dsh-shell": "0.2.1-alpha.1",
|
|
113
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.1"
|
|
114
|
-
},
|
|
115
106
|
"devDependencies": {
|
|
116
|
-
"@deepseek-ai/
|
|
117
|
-
"@deepseek-ai/dsh-
|
|
118
|
-
"@deepseek-ai/dsh-
|
|
119
|
-
"@
|
|
120
|
-
"@
|
|
107
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
108
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
|
|
109
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
|
|
110
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.2",
|
|
111
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.2",
|
|
112
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
|
|
113
|
+
"@deepseek-ai/dsh-output-retention": "0.2.1-alpha.2",
|
|
114
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
|
|
115
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
116
|
+
"@deepseek-ai/dsh-shell": "0.2.1-alpha.2",
|
|
117
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
|
|
118
|
+
"@types/node": "26.6.5",
|
|
119
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
121
120
|
"eslint-plugin-sonarjs": "4.2.2",
|
|
122
|
-
"oxfmt": "0.
|
|
123
|
-
"oxlint": "1.
|
|
124
|
-
"oxlint-tsgolint": "7.0.
|
|
125
|
-
"rolldown": "1.2.
|
|
121
|
+
"oxfmt": "0.72.0",
|
|
122
|
+
"oxlint": "1.87.0",
|
|
123
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
124
|
+
"rolldown": "1.2.13",
|
|
126
125
|
"typescript": "7.0.2",
|
|
127
|
-
"vite": "8.3.
|
|
128
|
-
"vitest": "5.0.
|
|
126
|
+
"vite": "8.3.4",
|
|
127
|
+
"vitest": "5.0.3"
|
|
129
128
|
},
|
|
130
129
|
"peerDependencies": {
|
|
131
|
-
"@deepseek-ai/
|
|
130
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
131
|
+
"@deepseek-ai/dsh": "^0.2.1-alpha.2",
|
|
132
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
|
|
133
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.2",
|
|
134
|
+
"@deepseek-ai/dsh-output-retention": "0.2.1-alpha.2",
|
|
135
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
136
|
+
"@deepseek-ai/dsh-shell": "0.2.1-alpha.2",
|
|
137
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2"
|
|
132
138
|
},
|
|
133
139
|
"peerDependenciesMeta": {
|
|
140
|
+
"@deepseek-ai/cordis": {
|
|
141
|
+
"optional": true
|
|
142
|
+
},
|
|
134
143
|
"@deepseek-ai/dsh": {
|
|
135
144
|
"optional": true
|
|
145
|
+
},
|
|
146
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
147
|
+
"optional": true
|
|
148
|
+
},
|
|
149
|
+
"@deepseek-ai/dsh-jobs": {
|
|
150
|
+
"optional": true
|
|
151
|
+
},
|
|
152
|
+
"@deepseek-ai/dsh-session": {
|
|
153
|
+
"optional": true
|
|
154
|
+
},
|
|
155
|
+
"@deepseek-ai/dsh-shell": {
|
|
156
|
+
"optional": true
|
|
157
|
+
},
|
|
158
|
+
"@deepseek-ai/dsh-tools": {
|
|
159
|
+
"optional": true
|
|
136
160
|
}
|
|
137
161
|
},
|
|
138
162
|
"icon": "icon.svg",
|
|
139
163
|
"engines": {
|
|
140
|
-
"dsh": ">=0.2.1-alpha.
|
|
164
|
+
"dsh": ">=0.2.1-alpha.2",
|
|
141
165
|
"node": "^22.19.0 || >=24.0.0"
|
|
142
166
|
},
|
|
143
167
|
"scripts": {
|