@jayyuen66/dsh-plugin-shared 0.1.0
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/LICENSE +21 -0
- package/README.md +230 -0
- package/config/oxlint.base.ts +1020 -0
- package/config/tsconfig.base.json +25 -0
- package/config/tsconfig.client.base.json +10 -0
- package/config/vitest.base.ts +78 -0
- package/dist/canonicalize-region-paths.js +63 -0
- package/dist/card-apply.js +20 -0
- package/dist/config/oxlint.base.js +855 -0
- package/dist/config/vitest.base.js +43 -0
- package/dist/errors.js +7 -0
- package/dist/http.js +121 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/job-outcome.js +18 -0
- package/dist/jsonl.js +20 -0
- package/dist/lesson-bus.js +43 -0
- package/dist/locale.js +43 -0
- package/dist/project-key.js +23 -0
- package/dist/record.js +11 -0
- package/dist/text.js +24 -0
- package/dist/tool-events.js +165 -0
- package/dist/trust.js +122 -0
- package/dist/types/canonicalize-region-paths.d.ts +7 -0
- package/dist/types/card-apply.d.ts +25 -0
- package/dist/types/config/oxlint.base.d.ts +109 -0
- package/dist/types/config/vitest.base.d.ts +29 -0
- package/dist/types/errors.d.ts +2 -0
- package/dist/types/http.d.ts +60 -0
- package/dist/types/job-outcome.d.ts +10 -0
- package/dist/types/jsonl.d.ts +8 -0
- package/dist/types/lesson-bus.d.ts +6 -0
- package/dist/types/locale.d.ts +39 -0
- package/dist/types/project-key.d.ts +9 -0
- package/dist/types/record.d.ts +4 -0
- package/dist/types/text.d.ts +10 -0
- package/dist/types/tool-events.d.ts +77 -0
- package/dist/types/trust.d.ts +22 -0
- package/icon.svg +1 -0
- package/package.json +146 -0
package/dist/trust.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { sendJson } from "./http.js";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
//#region lib/trust.ts
|
|
4
|
+
/** 回环主机名的两个字面量(IPv6 在 WHATWG URL 里**带方括号**)。 */
|
|
5
|
+
const LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "[::1]"]);
|
|
6
|
+
/** 唯一允许通过的 `sec-fetch-site` 取值(缺失=非浏览器,走第 1 条分歧的本地面)。 */
|
|
7
|
+
const SAME_SITE_ALLOW = /* @__PURE__ */ new Set(["same-origin", "none"]);
|
|
8
|
+
/** 站点级拒绝的响应文案(沿用 `lib/http.ts` 既有那句,统一口径时就少改一处断言)。 */
|
|
9
|
+
const CROSS_ORIGIN_TEXT = "cross-origin request rejected";
|
|
10
|
+
/** Host 权威不是本机可信地址时的响应文案。 */
|
|
11
|
+
const UNTRUSTED_HOST_TEXT = "untrusted host authority";
|
|
12
|
+
/** Origin 那条腿的结论(三形共用:沙箱 `null` 源、Origin 解析失败、权威不相等)。 */
|
|
13
|
+
const BAD_ORIGIN_VERDICT = "bad-origin";
|
|
14
|
+
/** 两形之外没有第三形(Node 的 IncomingHttpHeaders 值是 `string | string[]`),故不做 typeof 自卫。 */
|
|
15
|
+
function singleHeader(req, name) {
|
|
16
|
+
const value = req.headers?.[name];
|
|
17
|
+
if (value === void 0) return null;
|
|
18
|
+
if (Array.isArray(value)) return value.length === 1 ? value.join("") : null;
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/** Host 头的可接受字形。WHATWG 解析会**静默吃掉** tab/CR/LF 并丢掉 `user@` 前缀,
|
|
22
|
+
* 于是 `local\thost:8787` 与 `evil.com@localhost:8787` 都能被折成"回环权威"(本机实测
|
|
23
|
+
* `new URL("http://" + "local\\thost:8787").hostname === "localhost"`)。浏览器发不出这些,
|
|
24
|
+
* 但这一腿是本仓唯一挡得住 DNS 重绑定的判据 ⇒ 先按字形拒,再交给解析。 */
|
|
25
|
+
const HOST_GLYPHS_RE = /^[A-Za-z0-9._\-[\]:%]+$/u;
|
|
26
|
+
/** WHATWG 解析一个 authority(`host:port`);失败返回 null。IPv6 必须已带方括号。 */
|
|
27
|
+
function authorityOf(authority) {
|
|
28
|
+
if (!HOST_GLYPHS_RE.test(authority)) return null;
|
|
29
|
+
try {
|
|
30
|
+
return new URL(`http://${authority}`);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** 回环判定:`localhost` / `[::1]` / 127/8 四段纯数字(逐条对齐官方 loopback-hostname.ts)。 */
|
|
36
|
+
function isLoopbackHostname(hostname) {
|
|
37
|
+
const lowered = hostname.toLowerCase();
|
|
38
|
+
if (LOOPBACK_HOSTNAMES.has(lowered)) return true;
|
|
39
|
+
const parts = lowered.split(".");
|
|
40
|
+
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/u.test(part) && Number(part) <= 255);
|
|
41
|
+
}
|
|
42
|
+
/** `os.networkInterfaces()` 的地址面:IPv6 加方括号(与 `URL.hostname` 同形),IPv4 原样。 */
|
|
43
|
+
function interfaceAddresses() {
|
|
44
|
+
const listed = [];
|
|
45
|
+
for (const infos of Object.values(os.networkInterfaces())) for (const { family, address } of infos ?? []) listed.push(family === "IPv6" ? `[${address}]` : address);
|
|
46
|
+
return listed;
|
|
47
|
+
}
|
|
48
|
+
/** Host 权威是不是本机可信地址:回环恒过;非回环只在**声明了非回环服务面**且该址确属本机网卡时过。 */
|
|
49
|
+
function isAcceptedAuthority(hostname, opts) {
|
|
50
|
+
if (isLoopbackHostname(hostname)) return true;
|
|
51
|
+
if (opts.servingNonLoopback !== true) return false;
|
|
52
|
+
return (opts.localInterfaceAddresses ?? interfaceAddresses()).includes(hostname);
|
|
53
|
+
}
|
|
54
|
+
/** `sec-fetch-site` 白名单(缺失等于非浏览器面,交 Host/Origin 两条腿判)。 */
|
|
55
|
+
function siteIsSameOrigin(req) {
|
|
56
|
+
const site = singleHeader(req, "sec-fetch-site");
|
|
57
|
+
return site === null || SAME_SITE_ALLOW.has(site);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 对端是不是回环。拿不到 socket(手搓请求的测试构造点、非 node:http 的适配层)时返回 null,
|
|
61
|
+
* 表示"这一维未知",交调用侧按只看头部的旧口径处理。
|
|
62
|
+
*/
|
|
63
|
+
function peerLoopback(req) {
|
|
64
|
+
const raw = req.socket?.remoteAddress;
|
|
65
|
+
if (typeof raw !== "string" || raw === "") return null;
|
|
66
|
+
const address = raw.replaceAll(/^\[|\]$/gu, "").replace(/^::ffff:/iu, "");
|
|
67
|
+
return isLoopbackHostname(address) || address === "::1";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Host 腿:返回解析出的权威、`null`(没带 Host 且对端不是可判的非回环),或 `"rejected"`。
|
|
71
|
+
*
|
|
72
|
+
* 缺 Host 只在**非回环对端**时拒——浏览器走不到这条路(HTTP/1.1 强制 Host、HTTP/2 强制
|
|
73
|
+
* `:authority`,且 Host/Origin/Sec-Fetch-* 由同一个 URL 上下文导出),能走到的只有本地裸 socket
|
|
74
|
+
* 与 Node 客户端(Node 确实收无 Host 的请求)。
|
|
75
|
+
*/
|
|
76
|
+
function authorityOfFrom(req, opts) {
|
|
77
|
+
const rawHost = req.headers?.host;
|
|
78
|
+
if (Array.isArray(rawHost) && rawHost.length !== 1) return "rejected";
|
|
79
|
+
const host = singleHeader(req, "host");
|
|
80
|
+
if (host === null) return peerLoopback(req) === false ? "rejected" : null;
|
|
81
|
+
const parsed = authorityOf(host);
|
|
82
|
+
if (parsed === null || !isAcceptedAuthority(parsed.hostname, opts)) return "rejected";
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 一次判据:Host 权威 → sec-fetch-site(白名单)→ Origin 逐字比对。
|
|
87
|
+
* 纯函数、不写响应;写响应的样板在 `guardTrust`。
|
|
88
|
+
*/
|
|
89
|
+
function requestTrust(req, opts = {}) {
|
|
90
|
+
const hostUrl = authorityOfFrom(req, opts);
|
|
91
|
+
if (hostUrl === "rejected") return "untrusted-host";
|
|
92
|
+
if (!siteIsSameOrigin(req)) return "cross-site";
|
|
93
|
+
const origin = singleHeader(req, "origin");
|
|
94
|
+
if (origin === null) return "trusted";
|
|
95
|
+
if (origin === "null" || hostUrl === null) return BAD_ORIGIN_VERDICT;
|
|
96
|
+
const originUrl = authorityOf(origin.replace(/^[a-z][a-z0-9+.-]*:\/\//iu, ""));
|
|
97
|
+
if (originUrl === null) return BAD_ORIGIN_VERDICT;
|
|
98
|
+
return originUrl.host === hostUrl.host ? "trusted" : BAD_ORIGIN_VERDICT;
|
|
99
|
+
}
|
|
100
|
+
/** 结论对应的响应文案(cross-site 与 bad-origin 同一句,两者都是"不是本站的浏览器请求")。 */
|
|
101
|
+
function trustRejectionText(verdict) {
|
|
102
|
+
return verdict === "untrusted-host" ? UNTRUSTED_HOST_TEXT : CROSS_ORIGIN_TEXT;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* handler 体的**第一条语句**该放的东西:非 trusted 即写一次 403 JSON 并返回 false。
|
|
106
|
+
* 返回 false 时调用方必须立即 return,不得继续读 body 或产生副作用。
|
|
107
|
+
*/
|
|
108
|
+
function guardTrust(req, res, opts = {}) {
|
|
109
|
+
const verdict = requestTrust(req, opts);
|
|
110
|
+
if (verdict === "trusted") return true;
|
|
111
|
+
if (res.headersSent || res.writableEnded) {
|
|
112
|
+
console.error("[shared/trust] rejecting after headers were sent:", verdict);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
sendJson(res, 403, {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: trustRejectionText(verdict)
|
|
118
|
+
});
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { guardTrust, requestTrust, trustRejectionText };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 产物文本的 canonical 形态:所有 `//#region` 行折成相对包根的形状,其余字节原样保留。
|
|
3
|
+
* @param code - builder 拿到的产物文本
|
|
4
|
+
* @param pkgDir - 包根绝对路径(builder 的 import.meta.dirname)
|
|
5
|
+
* @returns 与 process.cwd() 无关的同一份文本;对 canonical 输入幂等
|
|
6
|
+
*/
|
|
7
|
+
export declare function canonicalizeRegionPaths(code: string, pkgDir: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
/**
|
|
3
|
+
* 客户端 apply 接收的 ctx 最小面。
|
|
4
|
+
* `effect` 逐字取官方 `Context['effect']`(installed
|
|
5
|
+
* `@deepseek-ai/cordis/lib/types/fiber.d.ts`:`interface Context extends
|
|
6
|
+
* Pick<Fiber, 'effect'>`,同步 `Disposable` 与可 await 的 `AsyncDisposable` 两条重载),
|
|
7
|
+
* 而不是本地重述一条 `(factory: () => (() => void) | undefined, label?) => void`。
|
|
8
|
+
* ⚠ 为什么这条要紧:各卡片的 `ClientCtx.effect` 现在也绑官方,而本地那份把 factory 的
|
|
9
|
+
* 值域写**宽**了(官方 `SyncEffect` 不收 `undefined`)。函数属性位按 contravariance 检查,
|
|
10
|
+
* 于是「官方 ctx」反而赋不进本投影——三个卡片包会各自撞出一个
|
|
11
|
+
* `TS2345 … Type 'undefined' is not assignable to type 'SyncEffect<any>'`,
|
|
12
|
+
* 并在每个消费包里用一个类型断言去过桥。守卫的 ctx 面归本模块所有,就在这里一次修好。
|
|
13
|
+
*/
|
|
14
|
+
export interface CardApplyCtx {
|
|
15
|
+
effect: Context["effect"];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* apply 幂等守卫:已 claim 返回 false(调用方直接 return),否则占位并
|
|
19
|
+
* 挂 ctx.effect 清理——HMR 重载时 flag 复位,再次 apply 可重新声明。
|
|
20
|
+
* @param ctx - apply 的 ctx(需要 effect)。
|
|
21
|
+
* @param flag - globalThis 上的布尔标记键名(如 "__xxxCardApplied")。
|
|
22
|
+
* @param label - effect 标签(日志/生命周期可观测)。
|
|
23
|
+
* @returns true 表示本次拿到声明权(继续执行),false 表示已被占位。
|
|
24
|
+
*/
|
|
25
|
+
export declare function claimApply(ctx: CardApplyCtx, flag: string, label: string): boolean;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { OxlintConfig, OxlintOverride } from "oxlint";
|
|
2
|
+
export type RuleTable = NonNullable<OxlintConfig["rules"]>;
|
|
3
|
+
/**
|
|
4
|
+
* 一条规则能被关掉的**全部**合法理由。写不进这六类的,就不该有关掉它的选项。
|
|
5
|
+
* 每类的判据来自 oxlint/eslint 官方文档、上游 recommended 集合的归属,加上本仓的实测。
|
|
6
|
+
*/
|
|
7
|
+
export type OffReason =
|
|
8
|
+
/** 规则的判据与官方契约方向相反,开了等于判官方写法违规。 */
|
|
9
|
+
"契约冲突"
|
|
10
|
+
/** 与本仓已启用的对侧规则互斥(开一侧就判另一侧死刑)。 */
|
|
11
|
+
| "对侧互斥"
|
|
12
|
+
/** 按它的意思改代码会把代码改坏,有实测的编译/运行后果。 */
|
|
13
|
+
| "实测否证"
|
|
14
|
+
/** 判据只在某种文体(用例文件 / 构建脚本)里成立,按文件种类收窄。 */
|
|
15
|
+
| "文体作用域"
|
|
16
|
+
/** 纯口味规则,上游不放进 recommended,开启只带来语义无关的重写。 */
|
|
17
|
+
| "上游不推荐"
|
|
18
|
+
/** 旧 `.oxlintrc.json` 继承下来、实测开了是 0 条 —— 没有存在的理由,直接作废。 */
|
|
19
|
+
| "零代价作废";
|
|
20
|
+
export interface OffJustification {
|
|
21
|
+
/** 关它的理由类别。 */
|
|
22
|
+
kind: OffReason;
|
|
23
|
+
/** 打开它在全仓产生的条数(口径见 OFF_JUSTIFICATIONS 的头注释)。 */
|
|
24
|
+
measured: number;
|
|
25
|
+
/** 一句话依据;配置加载只要求这个字段非空。 */
|
|
26
|
+
note: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 实测口径:临时把基线里每条 `off`(rules 与 override 层都算)翻成 `error` 再跑 15 个成员、
|
|
30
|
+
* 与基线取差数 `::error` 行,按路径分成 prod / test / scripts 三桶。`--deny=<rule>` 盖不住
|
|
31
|
+
* `off`,最早一轮用它在 13 个包目录上量出的数因此全偏小(`vitest/no-hooks` 用 `-D` 量出来是
|
|
32
|
+
* 0,实际是 181),整批作废重测。
|
|
33
|
+
*
|
|
34
|
+
* 键支持插件级通配(`vitest/*`),作用于该插件下没有更具体键的全部规则。
|
|
35
|
+
*/
|
|
36
|
+
export declare const OFF_JUSTIFICATIONS: Record<string, OffJustification>;
|
|
37
|
+
/**
|
|
38
|
+
* 旧 `.oxlintrc.json` 里继承来、此次**取消豁免**且确实零代价的规则。
|
|
39
|
+
*
|
|
40
|
+
* 这里原来记着三条。"零代价"那句话是用 `oxlint --deny=<rule>` 量的,而 `--deny` 盖不住
|
|
41
|
+
* override 层的 off(本文件头注释里有这条实测口径)——重测之后:`eslint/no-continue` 在
|
|
42
|
+
* `scripts/` 有 53 条、`eslint/no-await-in-loop` 有 2 条,于是这两条按文体收窄回
|
|
43
|
+
* `SCRIPTS_OVERRIDES`(TS 侧仍 0 条、仍生效),从这份"已作废"名单里撤掉。留在这儿的
|
|
44
|
+
* 才是真正确认过零代价的。
|
|
45
|
+
*/
|
|
46
|
+
export declare const RETIRED_OFFS: string[];
|
|
47
|
+
export declare const READONLY_SYNC_ALLOWLIST: string[];
|
|
48
|
+
export declare const BASE_RULES: RuleTable;
|
|
49
|
+
/**
|
|
50
|
+
* 用例文件(含 `test/*.ts` 夹具与 helper)共享的豁免:规模类,以及「桩件要跨官方名义类型」
|
|
51
|
+
* 的类型类判据。vitest 的用例结构类判据不在这里 —— 它们在基线整族 off、只在
|
|
52
|
+
* TEST_FILE_OVERRIDES 里开回来,所以 helper 模块天然不在射程内,旧配置那份按目录豁免的
|
|
53
|
+
* TEST_HELPER_OVERRIDES 因此作废。
|
|
54
|
+
*/
|
|
55
|
+
export declare const TEST_OVERRIDES: OxlintOverride;
|
|
56
|
+
/**
|
|
57
|
+
* vitest 的用例结构类判据**只**在这里生效。
|
|
58
|
+
* `expect-expect` 的 `assertFunctionNames` 取 vitest flavor 的官方默认
|
|
59
|
+
* (`["expect", "expectTypeOf", "assert", "assertType"]`):本仓用例里有相当一部分用
|
|
60
|
+
* `node:assert/strict` 断言,那不是「缺断言」,而是这条规则在 jest flavor 默认值下漏判
|
|
61
|
+
* ——实测 17 条里 5 条属于这一族,补上默认值后就不报了。
|
|
62
|
+
*
|
|
63
|
+
* 剩下两族用规则自带的选项解决,不关规则、也不改判据方向:
|
|
64
|
+
* - 断言写在包内 helper 里(`assertSameLedger()` 一类):`assertFunctionNames` 就是
|
|
65
|
+
* "哪些函数算断言"的名单,走 {@link makeTestFileOverrides} 的 `assertionHelpers` 报名字。
|
|
66
|
+
* 名单只收**裸标识符**:带 `*` / `.` 的通配会把"任何调用都算断言"写进配置,等于掏空规则,直接抛。
|
|
67
|
+
* - 标题以大写术语开头(`PTC`、`W5`、`B1` 这类档位/模式名):`allowedPrefixes` 是
|
|
68
|
+
* prefer-lowercase-title 给的同一条口径(官方例子就是 HTTP 方法前缀)。走 `titlePrefixes` 报名字。
|
|
69
|
+
* 反过来的做法——把标题改成 `pTC` / `w5`——会把领域名字改成坏名字,本仓不采取。
|
|
70
|
+
*/
|
|
71
|
+
export declare const VITEST_ASSERTION_DEFAULTS: string[];
|
|
72
|
+
export declare const TEST_FILE_OVERRIDES: OxlintOverride;
|
|
73
|
+
/**
|
|
74
|
+
* 生成用例文件的 override(vitest 结构类判据的开与关都在这张表里)。
|
|
75
|
+
* @param assertionHelpers - 本包"内部会断言"的 helper 函数名,并入 `expect-expect` 的名单。
|
|
76
|
+
* @returns 可直接放进 `overrides` 的配置项。
|
|
77
|
+
*/
|
|
78
|
+
export interface TestFileOverridesOptions {
|
|
79
|
+
/** 本包"内部会断言"的 helper 函数名,并入 `vitest/expect-expect` 的名单。 */
|
|
80
|
+
assertionHelpers?: string[];
|
|
81
|
+
/** 允许作为用例/套件标题开头的大写术语,并入 `vitest/prefer-lowercase-title` 的 allowedPrefixes。 */
|
|
82
|
+
titlePrefixes?: string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 生成用例文件的 override(vitest 结构类判据的开与关都在这张表里)。
|
|
86
|
+
* @param opts - 本包需要用规则自带选项登记的名单(见文件头的两族说明)。
|
|
87
|
+
* @returns 可直接放进 `overrides` 的配置项。
|
|
88
|
+
*/
|
|
89
|
+
export declare function makeTestFileOverrides(opts?: TestFileOverridesOptions): OxlintOverride;
|
|
90
|
+
export declare const SCRIPTS_OVERRIDES: OxlintOverride;
|
|
91
|
+
export interface PluginLintOptions {
|
|
92
|
+
syncWrites?: string[];
|
|
93
|
+
extraRules?: RuleTable;
|
|
94
|
+
extraOverrides?: OxlintOverride[];
|
|
95
|
+
extraIgnorePatterns?: string[];
|
|
96
|
+
/** 追加 oxlint JS plugin(alpha、不受 semver 保护);包内自定义规则走这里。 */
|
|
97
|
+
jsPlugins?: NonNullable<OxlintConfig["jsPlugins"]>;
|
|
98
|
+
/** 包级豁免:每个 off 都要在这里写明 kind / measured / note,否则配置加载即抛错。 */
|
|
99
|
+
offReasons?: Record<string, OffJustification>;
|
|
100
|
+
/**
|
|
101
|
+
* 本包"内部会断言"的用例 helper 名(如 `assertSameLedger`)。报进来的名字并入
|
|
102
|
+
* `vitest/expect-expect` 的 `assertFunctionNames`,规则本身保持 error —— 这是规则给的口径,
|
|
103
|
+
* 不是豁免。只收裸标识符,通配会掏空这条规则,直接抛。
|
|
104
|
+
*/
|
|
105
|
+
assertionHelpers?: string[];
|
|
106
|
+
/** 允许开头的用例标题大写术语(`PTC`/`W5`/`B1` 这类档位或模式名),进 allowedPrefixes。 */
|
|
107
|
+
titlePrefixes?: string[];
|
|
108
|
+
}
|
|
109
|
+
export declare function definePluginConfig(options?: PluginLintOptions): OxlintConfig;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ViteUserConfig } from "vitest/config";
|
|
2
|
+
/**
|
|
3
|
+
* 类型名按实测取(四轮才钉准,过程记在这里免得下次再猜):
|
|
4
|
+
* - `import type { UserConfig } from "vitest/config"` ⇒ TS2305,5.0.2 不导出这个名字;
|
|
5
|
+
* - `import type { UserConfig as ViteUserConfig }` 同样 TS2305 —— 它在 `vitest/config` 里的
|
|
6
|
+
* **导出名就已经是 `ViteUserConfig`**(源上是 `export { UserConfig as ViteUserConfig } from 'vite'`),
|
|
7
|
+
* 别名要写在被导入的那一侧,不能自己再造一遍;
|
|
8
|
+
* - `Parameters<typeof defineConfig>[0]` ⇒ 合出来是重载联合 `ViteUserConfigExport`(函数类型),
|
|
9
|
+
* 取 `.test` 直接 TS2339,并级联两条 `no-unsafe-*`;
|
|
10
|
+
* - `TestUserConfig` 是 **test 块本身**的类型,不是配置文件对象类型,
|
|
11
|
+
* 拿它当返回值会在 `exactOptionalPropertyTypes: true` 下报 TS2375。
|
|
12
|
+
* 正确的那个是 `ViteUserConfig`:vitest 用模块声明合并给它加了 `test?: VitestInlineConfig`,
|
|
13
|
+
* 也正是 `defineConfig(config: UserConfig): UserConfig` 那条重载吃的形状。
|
|
14
|
+
*/
|
|
15
|
+
type PackageConfig = ViteUserConfig;
|
|
16
|
+
/** 覆盖率面与 test 块里真正因包而异的那一项。 */
|
|
17
|
+
export interface PackageVitestOptions {
|
|
18
|
+
/** `coverage.include` —— 本包该被统计的源文件(各包只有这里不同)。 */
|
|
19
|
+
coverageInclude: string[];
|
|
20
|
+
/** 追加/覆盖 test 字段(少用;出现即应在包内配置里写明为什么)。 */
|
|
21
|
+
test?: PackageConfig["test"];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 公共面:13 份配置里逐字相同的那部分,从今往后才真的只有一处。
|
|
25
|
+
* `testTimeout: 20_000` 是 12 个包的共同值(vitest 默认 5000),shared 原先没写、
|
|
26
|
+
* 现在继承这个值只会放宽不会收紧,故不会把任何已过用例变成不稳定。
|
|
27
|
+
*/
|
|
28
|
+
export declare function definePackageConfig(options: PackageVitestOptions): PackageConfig;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* 下面的 header 读取 helper 不只被真 `IncomingMessage` 调用:`test/trust*.test.ts` 手搓合成
|
|
4
|
+
* 请求、以及非 node:http 的适配层都会传只带部分字段的对象。Node 的类型把 `headers` /
|
|
5
|
+
* `socket` 都标成必存在,照抄那个形状会让类型面比现实更乐观 —— 运行时不得不写 `?.` 自卫,
|
|
6
|
+
* 而那条自卫又会被类型感知规则判成冗余。所以这里如实声明可缺字段的入参类型。
|
|
7
|
+
*/
|
|
8
|
+
export interface PartialRequest {
|
|
9
|
+
readonly headers?: IncomingHttpHeaders;
|
|
10
|
+
readonly socket?: {
|
|
11
|
+
readonly remoteAddress?: string | undefined;
|
|
12
|
+
} | null;
|
|
13
|
+
readonly url?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** helper 接受的请求:真 `IncomingMessage` 或合成片段。 */
|
|
16
|
+
export type HttpRequest = IncomingMessage | PartialRequest;
|
|
17
|
+
/** 写 JSON 响应(no-store:端点都是运行时状态,禁缓存防陈旧)。
|
|
18
|
+
* 已发头/已结束则静默跳过:同一响应被写两次会抛 ERR_HTTP_HEADERS_SENT,
|
|
19
|
+
* 而端点多在 async 回调里,抛错变成 unhandled rejection 拖垮共享的 web host。 */
|
|
20
|
+
export declare function sendJson(res: ServerResponse, status: number, payload: unknown): void;
|
|
21
|
+
/**
|
|
22
|
+
* CORS 同源纵深防御:浏览器请求带 sec-fetch-site,非 same-origin/none 即拒绝。
|
|
23
|
+
* 空头(curl/CLI)不拦——浏览器侧伪造不了同源判定,CLI 是本地可信调用面。
|
|
24
|
+
*/
|
|
25
|
+
export declare function isCrossOrigin(req: IncomingMessage): boolean;
|
|
26
|
+
/** 从 req.url 读 query 参数(URLSearchParams 语义:+ 解码空格、尊重 ; 分隔等)。 */
|
|
27
|
+
export declare function queryParam(req: IncomingMessage, name: string): string | null;
|
|
28
|
+
/** CSRF 校验:header 值非空且等于下发 token(sec-fetch-site 只防浏览器,可伪造)。 */
|
|
29
|
+
export declare function checkCsrf(req: IncomingMessage, token: string, headerName: string): boolean;
|
|
30
|
+
/** readBody 的失败原因:`too-large` 超限(413),`aborted` 流读取中断/坏流(400)。 */
|
|
31
|
+
export type BodyRead = {
|
|
32
|
+
readonly ok: true;
|
|
33
|
+
readonly text: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly ok: false;
|
|
36
|
+
readonly reason: "too-large" | "aborted";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 读 POST body(**按 UTF-8 字节**计上限;先查 content-length 再收流)。
|
|
40
|
+
*
|
|
41
|
+
* 两个必须一次做对的点:
|
|
42
|
+
* 1. 逐块 `String(chunk)` 会把跨 chunk 边界的多字节字符(中文/emoji)解码成
|
|
43
|
+
* U+FFFD 且仍是合法 JSON —— 静默写坏用户数据。故累积 Buffer 后一次性解码。
|
|
44
|
+
* 2. 只按解码后字符数设限,恶意 body 可在触顶前已占满内存;content-length
|
|
45
|
+
* 预检让明显超限的请求根本不开始累积。
|
|
46
|
+
* 超限/坏流都立即停止读取(for-await 提前 return 会 release 迭代器并销毁流)。
|
|
47
|
+
*/
|
|
48
|
+
export declare function readBody(req: IncomingMessage, maxBytes: number): Promise<BodyRead>;
|
|
49
|
+
/**
|
|
50
|
+
* 一次性校验组合(通用 POST 端点样板):跨域拒绝 → CSRF 拒绝 → 读 body →
|
|
51
|
+
* 413(超限)/400(流中断)。全过返回 body 字符串。缺 CSRF 规范(如 GET 或
|
|
52
|
+
* 自持 token 端点)时传 skipCsrf=true。
|
|
53
|
+
*/
|
|
54
|
+
export declare function guardBody(req: IncomingMessage, res: ServerResponse, opts: {
|
|
55
|
+
maxBytes: number;
|
|
56
|
+
csrf?: {
|
|
57
|
+
token: string;
|
|
58
|
+
headerName: string;
|
|
59
|
+
};
|
|
60
|
+
}): Promise<string | null>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ShellProcess } from "@deepseek-ai/dsh-shell";
|
|
2
|
+
import type { JobOutcome } from "@deepseek-ai/dsh-jobs";
|
|
3
|
+
/** 映射只用得到的三个成员(不是整个 ShellProcess:生产者句柄的其余面与本函数无关)。 */
|
|
4
|
+
export type SettledProcess = Pick<ShellProcess, "status" | "exitCode" | "signal">;
|
|
5
|
+
/**
|
|
6
|
+
* 落定进程 → 作业结局。
|
|
7
|
+
* @param proc 已落定的子进程句柄(`done` 已 resolve 之后再读,值才是终态)
|
|
8
|
+
* @returns 交给 {@link JobRegistry} 生产者的结局:killed / completed,理由在 detail
|
|
9
|
+
*/
|
|
10
|
+
export declare function jobOutcomeOf(proc: SettledProcess): JobOutcome;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 反复保留后半行段直到 UTF-8 字节数 ≤ `maxBytes`;行数 ≤2 时停手(单条超限行原样保留,
|
|
3
|
+
* 防死循环也防清空文件)。未超限直接返回入参**同一引用**,调用方可据此跳过写盘。
|
|
4
|
+
* @param text 整份 JSONL 文本
|
|
5
|
+
* @param maxBytes 字节上限
|
|
6
|
+
* @returns 收缩后的文本
|
|
7
|
+
*/
|
|
8
|
+
export declare function shrinkJsonlTail(text: string, maxBytes: number): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** 支持的界面/模型侧文案语言。 */
|
|
2
|
+
export type Locale = "zh" | "en";
|
|
3
|
+
/** 缺省语言。设置项未设或值不合语法时落到这里。 */
|
|
4
|
+
export declare const DEFAULT_LOCALE: Locale;
|
|
5
|
+
/**
|
|
6
|
+
* 官方 `@deepseek-ai/dsh-client-locale` 在 Host settings 文档里拥有的命名空间与字段名
|
|
7
|
+
* (其 `lib/index.js:5-7`)。这里是**抄常量不是引依赖**:那是宿主内部实现,没进公开
|
|
8
|
+
* 依赖面,而两侧必须同名才能读到同一份偏好。
|
|
9
|
+
*/
|
|
10
|
+
export declare const LOCALE_SETTINGS_NAMESPACE = "locale";
|
|
11
|
+
export declare const LOCALE_PREFERENCE_FIELD = "preference";
|
|
12
|
+
/** 一个包的全部消息键值表(键名即语义,值是人读文案)。 */
|
|
13
|
+
export interface MessagesCatalog<Messages> {
|
|
14
|
+
readonly zh: Messages;
|
|
15
|
+
readonly en: Messages;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 把设置里的原始值归一成受支持的 locale:按**主语言子标签**判定(`en-US`、`zh-Hans-CN`
|
|
19
|
+
* 都落位),与 client 半「先完整标签、再主语言子标签」的规则同源;判不出来一律退
|
|
20
|
+
* DEFAULT_LOCALE——文案语言不该成为报错面。
|
|
21
|
+
* @param raw - 设置里的原始值,类型不可信(来自用户文档)。
|
|
22
|
+
* @returns 归一后的 locale。
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveLocale(raw: unknown): Locale;
|
|
25
|
+
/**
|
|
26
|
+
* 从 `ctx.settings.describe(LOCALE_SETTINGS_NAMESPACE)` 的返回值里取偏好并归一。
|
|
27
|
+
* describe 出来的形状由别的包拥有,所以入参按 unknown 处理:非对象、数组、缺字段
|
|
28
|
+
* 都走默认语言,不抛。
|
|
29
|
+
* @param described - describe 的原始返回值(可能 undefined)。
|
|
30
|
+
* @returns 归一后的 locale。
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveLocalePreference(described: unknown): Locale;
|
|
33
|
+
/**
|
|
34
|
+
* 取某个语言下的消息表。
|
|
35
|
+
* @param catalog - 两语齐全的消息表。
|
|
36
|
+
* @param locale - 已归一的语言。
|
|
37
|
+
* @returns 对应语言的消息表(引用返回,不做拷贝)。
|
|
38
|
+
*/
|
|
39
|
+
export declare function messagesFor<Messages>(catalog: MessagesCatalog<Messages>, locale: Locale): Messages;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 可注入面:realpath 是同步 fs 调用,测试替换它来观察软链归一与失败兜底两条臂。 */
|
|
2
|
+
export interface ProjectKeyOptions {
|
|
3
|
+
realpath?: (target: string) => string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* cwd → 项目桶键。非字符串 / trim 后为空 / 归一后落到根目录 → `"default"`
|
|
7
|
+
* (与记忆网关的兜底桶名一致)。
|
|
8
|
+
*/
|
|
9
|
+
export declare function deriveProjectKey(cwd: unknown, opts?: ProjectKeyOptions): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 保留前 `maxChars` 个码元;切点切开代理对时丢掉尾部那个孤立高代理。
|
|
3
|
+
* `maxChars <= 0` 与 NaN ⇒ 空串。
|
|
4
|
+
*/
|
|
5
|
+
export declare function truncateEnd(text: string, maxChars: number): string;
|
|
6
|
+
/**
|
|
7
|
+
* 保留后 `minChars` 个码元;切点切开代理对时丢掉首部那个孤立低代理。
|
|
8
|
+
* `minChars <= 0` 与 NaN 与 `truncateEnd(text, 0)` 同口径返回空串,不返回整串。
|
|
9
|
+
*/
|
|
10
|
+
export declare function truncateStart(text: string, minChars: number): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
2
|
+
export type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
3
|
+
export type { PtcDispatchEventData, PtcDispatchStartEventData } from "@deepseek-ai/dsh-tools/types";
|
|
4
|
+
/** 一条 tool/call 的记账记录。 */
|
|
5
|
+
export interface ToolCallRecord {
|
|
6
|
+
/** 调用名(edit/write/str_replace_editor/read/grep/…)。 */
|
|
7
|
+
name: string | undefined;
|
|
8
|
+
/** callId:tool/result 串联键;缺失时由调用方按 seq 造 `nc:` 键。 */
|
|
9
|
+
callId: string | undefined;
|
|
10
|
+
/** 解析后的参数对象(解析失败/非对象时为空对象,配合 badArguments 使用)。 */
|
|
11
|
+
arguments: Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* arguments 原始值无法解析为对象(JSON 字符串非法、或非字符串非对象)。
|
|
14
|
+
* 调用方据此保留各自语义:quality-gate 判 EDIT_SKIP、danger-guard 跳过记账——
|
|
15
|
+
* 共享层只标记不决策,避免把两插件的"坏参是否记账"策略悄悄统一。
|
|
16
|
+
*/
|
|
17
|
+
badArguments: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 在**入参数组**中的下标(归因/游标用),不是官方事件自带的 `SessionSeq`:消费方按
|
|
20
|
+
* 窗口切片读取事件流,切片下标才是它们游标语义里的那个「序」。
|
|
21
|
+
*/
|
|
22
|
+
seq: number;
|
|
23
|
+
}
|
|
24
|
+
/** 一条 tool/result 的成败。 */
|
|
25
|
+
export interface ToolResultRecord {
|
|
26
|
+
callId: string | undefined;
|
|
27
|
+
isError: boolean;
|
|
28
|
+
seq: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* tool/result 的 isError 判定(官方 v4:成败位在 message 级、类型为 `?: boolean`;
|
|
32
|
+
* 字段缺失按成功)。非 tool/result 事件一律按成功——PTC 子调度的成败位在 data 顶层,
|
|
33
|
+
* 由 `resultRecordOf` 的对应分支处理,不从这儿走。
|
|
34
|
+
*/
|
|
35
|
+
export declare function resultIsError(event: SessionEvent): boolean;
|
|
36
|
+
/** 解析 tool/call 的 arguments:string JSON 文本或已解析对象;非法/缺失 → 空对象。 */
|
|
37
|
+
export declare function parseToolArguments(raw: unknown): Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* arguments 是否「无法参与记账」:仅当字符串 JSON **语法非法**(JSON.parse 抛错)。
|
|
40
|
+
* 合法 JSON 非对象("42")、undefined/数字/对象等一律不标——quality-gate 原实现
|
|
41
|
+
* 对 JSON.parse 抛错判 EDIT_SKIP,但对"解析成功却非对象"仍记账(path=undefined),
|
|
42
|
+
* 本标记必须精确复用那一个语义点,不能把"非对象"也划进坏参。
|
|
43
|
+
*/
|
|
44
|
+
export declare function toolArgumentsBad(raw: unknown): boolean;
|
|
45
|
+
/** 一条事件产出的记账行(call 与 result 互斥;非记账事件两位都是 undefined)。 */
|
|
46
|
+
export interface ToolEventRows {
|
|
47
|
+
call: ToolCallRecord | undefined;
|
|
48
|
+
result: ToolResultRecord | undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* **单条事件**的记账行(`scanToolEvents` 的一步,也是它唯一的实现处)。
|
|
52
|
+
*
|
|
53
|
+
* 之所以导出:danger-guard 把同一套台账折成 `sessionProjections` 投影单元(增量折叠)后,
|
|
54
|
+
* 折叠与"注册表缺席时的全量扫描"必须逐字同源——两处各写一遍字段读取,就会在两行上给出
|
|
55
|
+
* 不同的证据链(那是安全门禁的 fail-open)。折叠侧传 `event.seq`,扫描侧传数组下标,
|
|
56
|
+
* 两者在官方 `seq = log.length` 的连号契约下同一个值。
|
|
57
|
+
*/
|
|
58
|
+
export declare function toolEventRowsOf(event: SessionEvent, index: number): ToolEventRows;
|
|
59
|
+
/**
|
|
60
|
+
* 扫描事件流 → tool/call 与 tool/result 两张表。
|
|
61
|
+
* 只收集形状可用的事件,其余静默跳过——调用方(门禁/证据链)以空表安全降级,不因零星
|
|
62
|
+
* 坏事件抛错。数组空洞(类型面不可表示、重放数据里却可能出现)同样跳过。
|
|
63
|
+
* seq 取事件在入参数组里的下标(与 danger-guard scanToolCalls 的归因语义一致)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function scanToolEvents(events: readonly SessionEvent[]): {
|
|
66
|
+
calls: ToolCallRecord[];
|
|
67
|
+
results: ToolResultRecord[];
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* 由 tool/call 记录推导被编辑的文件路径;str_replace_editor 的 view 只读不算写。
|
|
71
|
+
* @returns { kind: "write", path } 写操作;{ kind: "read-view", path } 只读视图;
|
|
72
|
+
* { kind: "skip" } 无路径或参数形状不可用(调用方忽略)。
|
|
73
|
+
*/
|
|
74
|
+
export declare function editPathOf(call: ToolCallRecord): {
|
|
75
|
+
kind: "write" | "read-view" | "skip";
|
|
76
|
+
path: string | undefined;
|
|
77
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/** 信任判据的结论。除 `trusted` 外都该被拒。 */
|
|
3
|
+
export type TrustVerdict = "trusted" | "untrusted-host" | "cross-site" | "bad-origin";
|
|
4
|
+
/** `requestTrust` / `guardTrust` 的可选入参。 */
|
|
5
|
+
export interface TrustOptions {
|
|
6
|
+
/** 宿主是否绑在非回环地址上(`webServer.host === "0.0.0.0"`)。缺省按 false(保守)。 */
|
|
7
|
+
servingNonLoopback?: boolean;
|
|
8
|
+
/** 注入本机地址面(测试用;缺省取 `os.networkInterfaces()` 的实际值)。 */
|
|
9
|
+
localInterfaceAddresses?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 一次判据:Host 权威 → sec-fetch-site(白名单)→ Origin 逐字比对。
|
|
13
|
+
* 纯函数、不写响应;写响应的样板在 `guardTrust`。
|
|
14
|
+
*/
|
|
15
|
+
export declare function requestTrust(req: IncomingMessage, opts?: TrustOptions): TrustVerdict;
|
|
16
|
+
/** 结论对应的响应文案(cross-site 与 bad-origin 同一句,两者都是"不是本站的浏览器请求")。 */
|
|
17
|
+
export declare function trustRejectionText(verdict: TrustVerdict): string;
|
|
18
|
+
/**
|
|
19
|
+
* handler 体的**第一条语句**该放的东西:非 trusted 即写一次 403 JSON 并返回 false。
|
|
20
|
+
* 返回 false 时调用方必须立即 return,不得继续读 body 或产生副作用。
|
|
21
|
+
*/
|
|
22
|
+
export declare function guardTrust(req: IncomingMessage, res: ServerResponse, opts?: TrustOptions): boolean;
|
package/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="36" height="36" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#658EFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path fill="#54ECE7" d="M14 6h3a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3H7a3 3 0 0 1 -3 -3V9a3 3 0 0 1 3 -3h3" opacity=".2" stroke="#54ECE7"/><path stroke="#658EFF" d="M14 6h3a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3H7a3 3 0 0 1 -3 -3V9a3 3 0 0 1 3 -3h3"/><path stroke="#658EFF" d="M4 11h16"/><path stroke="#658EFF" d="M12 6v5"/></g></svg>
|