dsh-plugin-update 0.2.0 → 0.3.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/CHANGELOG.md +49 -0
- package/README.md +483 -279
- package/dist/batch-run.d.ts +80 -0
- package/dist/batch-run.js +86 -0
- package/dist/batch.d.ts +111 -0
- package/dist/batch.js +183 -0
- package/dist/changelog-io.d.ts +56 -0
- package/dist/changelog-io.js +258 -0
- package/dist/changelog.d.ts +96 -0
- package/dist/changelog.js +287 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +75 -1
- package/dist/commands.d.ts +59 -0
- package/dist/commands.js +60 -4
- package/dist/config.d.ts +48 -0
- package/dist/config.js +9 -1
- package/dist/diag.d.ts +125 -0
- package/dist/diag.js +243 -0
- package/dist/entry.d.ts +60 -0
- package/dist/entry.js +305 -0
- package/dist/gate.d.ts +35 -0
- package/dist/host-batch.d.ts +113 -0
- package/dist/host-batch.js +778 -0
- package/dist/host.d.ts +129 -0
- package/dist/host.js +479 -41
- package/dist/panel-batch.d.ts +151 -0
- package/dist/panel-batch.js +927 -0
- package/dist/panel.d.ts +256 -0
- package/dist/panel.js +1300 -0
- package/dist/ports.d.ts +208 -0
- package/dist/queue.d.ts +109 -0
- package/dist/queue.js +182 -0
- package/dist/reader.d.ts +84 -0
- package/dist/reader.js +6 -4
- package/dist/redaction.d.ts +120 -0
- package/dist/redaction.js +205 -0
- package/dist/service.d.ts +72 -0
- package/dist/service.js +100 -6
- package/dist/store.d.ts +229 -0
- package/dist/store.js +251 -12
- package/package.json +27 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/redaction.ts —— 失败详情脱敏规则表(#19,承接 #6;#24 单源收敛)。
|
|
3
|
+
*
|
|
4
|
+
* 五条具名规则,顺序固定。处理方式只有两种:替换成占位符,或丢弃整项。
|
|
5
|
+
* 判据是机械的:秘密片段的起止能否被规则无歧义定位(与 #5 风险序一致:误导 > 缺失)。
|
|
6
|
+
*
|
|
7
|
+
* - 绝对路径 → 替换为 `<路径>`(#5 冻结,不动)。路径终止于空白,起止唯一。
|
|
8
|
+
* - URL 用户信息(scheme://user:pass@)→ 丢弃整项。密码自身可能含 `@`,起止不唯一。
|
|
9
|
+
* - 令牌前缀(npm / GitHub / sk / Bearer 形状)→ 替换为 `<脱敏>`。
|
|
10
|
+
* - 密码键值对(token / password / api_key / cookie 形状)→ 替换为 `<脱敏>`(保留键名与分隔符,只换值)。
|
|
11
|
+
* - 邮箱 → 替换为 `<脱敏>`。
|
|
12
|
+
*
|
|
13
|
+
* 只有两个占位符:`<路径>` 与 `<脱敏>`。不为每种秘密建一套分类学。
|
|
14
|
+
*
|
|
15
|
+
* 处理顺序固定为:压平空白 → URL 用户信息(命中即整项丢弃)→ 路径 → 令牌 → 键值对 → 邮箱
|
|
16
|
+
* → 空白边界封顶(封顶后不再扫描)。顺序的目的是输出确定性(同一输入两次运行逐字节相同),
|
|
17
|
+
* 封顶在脱敏之后,且封顶之后不再扫描。
|
|
18
|
+
*
|
|
19
|
+
* 本模块是唯一的规则源(#24 单源收敛):宿主侧(store.ts 经 sanitizeDetail)与面板侧
|
|
20
|
+
* (panel.ts 经 redactForCopy)同缝消费,不再各存一份 PANEL_* 镜像。面板侧的复制预算
|
|
21
|
+
* 1500 字也在此单源定义(COPY_BUDGET_CHARS),面板只转出口。
|
|
22
|
+
* 本模块零 Node 导入(只用 URL / TextEncoder 全局),宿主与浏览器闭包两边都进得去。
|
|
23
|
+
*
|
|
24
|
+
* 绝不外发的三样按构造问题处理,不在文字层拦:
|
|
25
|
+
* 响应正文与安装输出全文(入口不接受此类参数)、调用栈(不构造)、内网主机名
|
|
26
|
+
* (不是高精度标记,是猜的——源信息隐私由源主机名字段承担,不在文字层拦)。
|
|
27
|
+
*
|
|
28
|
+
* 已知缺口(显式 known-gap,不静默修):`file:///tmp/x` 三斜杠形状会被当成 URL 放过
|
|
29
|
+
* (`file://` 的前置带着冒号),见 KNOWN_GAP_FILE_URL_PREFIX 与测试里的显式断言。
|
|
30
|
+
* UNC 反斜杠本轮已覆盖(#19 落地时)。
|
|
31
|
+
*/
|
|
32
|
+
/** 失败详情封顶字数:回给用户的是一行人话,不是整段安装输出(与旧口径同值,落刀点改为感白边界)。 */
|
|
33
|
+
export declare const DETAIL_MAX_CHARS = 300;
|
|
34
|
+
/**
|
|
35
|
+
* 复制文本预算(字符):面板复制诊断全文的上限,超限在空白边界截断。
|
|
36
|
+
* 单源定义于此(#24 收敛前散在 panel.ts,值同为 1500,行为收敛后只此一处)。
|
|
37
|
+
*/
|
|
38
|
+
export declare const COPY_BUDGET_CHARS = 1500;
|
|
39
|
+
/**
|
|
40
|
+
* 内联证据预算(字节):#5 定深证据分界约 1KB,300 字中文即 900 字节,
|
|
41
|
+
* 加上溯源信息很可能越线。#24 起运行时强制执行(见 enforceDiagBudget),
|
|
42
|
+
* 测试对着这个常量断言。
|
|
43
|
+
*/
|
|
44
|
+
export declare const DIAG_INLINE_BUDGET_BYTES = 1024;
|
|
45
|
+
/** 脱敏占位符:路径是唯一结构上可识别的类别,单占位;其余全部共用一个。 */
|
|
46
|
+
export declare const REDACTED_PATH = "<\u8DEF\u5F84>";
|
|
47
|
+
export declare const REDACTED_SECRET = "<\u8131\u654F>";
|
|
48
|
+
/**
|
|
49
|
+
* 已知缺口前缀:`file:///` 三斜杠形状原样放过(URL 与文件系统路径的边界模糊)。
|
|
50
|
+
* 显式记为 known-gap:不断言它会被掩掉,而是断言它原样通过且已被记录。
|
|
51
|
+
* 若将来要覆盖,需另立规格(属 URL 与路径边界的产品决定,不顺手修)。
|
|
52
|
+
*/
|
|
53
|
+
export declare const KNOWN_GAP_FILE_URL_PREFIX = "file:///";
|
|
54
|
+
/**
|
|
55
|
+
* 五条规则的真实可引用标识(固定顺序):事件清单的 `rules` 栏引用这些名字,
|
|
56
|
+
* 而不必复述匹配形态(见 src/gate.ts)。顺序即应用顺序,改顺序即改行为。
|
|
57
|
+
*/
|
|
58
|
+
export declare const REDACTION_RULE_NAMES: readonly ["R_ABSOLUTE_PATH", "R_URL_USERINFO", "R_TOKEN", "R_CRED_PAIR", "R_EMAIL"];
|
|
59
|
+
export type RedactionRuleName = (typeof REDACTION_RULE_NAMES)[number];
|
|
60
|
+
/** 本包认可的源主机名集合:「一律官方源」政策本来就蕴含的集合,不是新增维护负担。 */
|
|
61
|
+
export declare const ENDORSED_REGISTRY_HOSTS: readonly ["registry.npmjs.org"];
|
|
62
|
+
/**
|
|
63
|
+
* 空白边界落刀(通用):max 处若在词中,退到上一个空白;末尾若把占位符切开(如 `<路`),
|
|
64
|
+
* 退到占位符起点。中文无空白时退无可退,按 max 硬切(长度不变,可见行为变化须进发布说明)。
|
|
65
|
+
*/
|
|
66
|
+
export declare function truncateToWordBoundary(text: string, max: number): string;
|
|
67
|
+
/**
|
|
68
|
+
* 把外面世界的脏错误收成一行:压平空白 → URL 用户信息(命中整项丢弃)→ 路径 → 令牌
|
|
69
|
+
* → 键值对 → 邮箱 → 300 字封顶(封顶后不再扫描)。
|
|
70
|
+
* URL 用户信息命中时丢弃整项(返回空串):密码自身可能含 `@`,起止不唯一,
|
|
71
|
+
* 替换会把一次泄漏变成一次静默的错(证据被销毁),丢弃至少看得出。
|
|
72
|
+
*/
|
|
73
|
+
export declare function sanitizeDetail(text: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* 复制前把一行文本收干净(#24 单源:与 sanitizeDetail 同规则同顺序,只换 1500 预算)。
|
|
76
|
+
* 确定性:同一输入两次运行逐字节相同。URL 用户信息同样整项丢弃(返空串),
|
|
77
|
+
* 调用方(panel.ts buildDiagnosticText)对空串回落到稳定码,不静默。
|
|
78
|
+
*/
|
|
79
|
+
export declare function sanitizeForCopy(text: unknown): string;
|
|
80
|
+
/**
|
|
81
|
+
* 源主机名规则(R 源信息):只在命中国家认可的源时给 origin 主机名,
|
|
82
|
+
* 不带用户信息、端口、路径、查询参数;客户端 IP 永不进;不认可即省略(省略本身即信息)。
|
|
83
|
+
* 本函数只做规则(供 #18 的 diag 落地时调用),本票不新增任何回包键。
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveRegistryHost(registryUrl: string): string | null;
|
|
86
|
+
/** 诊断块 JSON 字节数(TextEncoder 全局,宿主与浏览器闭包两边都有;无则退化为字符数)。 */
|
|
87
|
+
export declare function diagByteLength(value: unknown): number;
|
|
88
|
+
/**
|
|
89
|
+
* 字节墙下永不丢的键:码 / 阶段 / 路由+方法(原子对,要么同有要么同省) / 请求编号
|
|
90
|
+
* (及版本门 `v` 与 envelope 码)。这些键即使超预算也保留——丢了它们诊断即不可定位,
|
|
91
|
+
* 不如超一点(实际仍能压进预算,见下)。
|
|
92
|
+
* 路由与方法正交但原子:构造侧(diag.ts deriveRouteMethod)同产同省,墙侧若只保路由
|
|
93
|
+
* 必拆散该对(对抗审查实锤:#21 超长中文用例 route 在 method 被省,well-formed 断言红)。
|
|
94
|
+
* 方法串极短(https/fs/phone/spawn/service,约十字节),永保代价可忽略。
|
|
95
|
+
*/
|
|
96
|
+
export declare const DIAG_NEVER_DROP_KEYS: readonly ["v", "code", "error", "errorKind", "stage", "route", "method", "requestId"];
|
|
97
|
+
/**
|
|
98
|
+
* 按序省的键(#24 票面顺序):registryHost → action → 版本(latest → running → target 包名)
|
|
99
|
+
* → 其余可选。尾段按诊断价值升序(先省最不定位的):耗时(仅范围断言)→ 退出码(仅 exec 有)
|
|
100
|
+
* → 环境(面板多已知)→ 检查编号(请求号已永保,join 一半冗余)
|
|
101
|
+
* → HTTP 状态(429 信号最定位,最后省)。方法不在此列(与路由原子永保)。
|
|
102
|
+
* 面板只渲染不推导,不靠顺序分支。
|
|
103
|
+
* (对抗审查订正:旧尾段把 latency 放最后即最难省,与“仅范围断言”价值倒挂,已掉头;
|
|
104
|
+
* 旧尾段含 method,已移入永保,理由见上。)
|
|
105
|
+
*/
|
|
106
|
+
export declare const DIAG_DROP_ORDER: readonly ["registryHost", "action", "latestVersion", "runningVersion", "targetPackageName", "latencyMs", "exitCode", "environmentKind", "checkId", "httpStatus"];
|
|
107
|
+
export type DiagDropKey = (typeof DIAG_DROP_ORDER)[number];
|
|
108
|
+
/**
|
|
109
|
+
* DIAG 字节墙 enforcement:输入任意 diag 候选对象,返回预算内的对象 + 显式 truncated 位。
|
|
110
|
+
*
|
|
111
|
+
* - 码/阶段/路由+方法(原子对)/请求编号(及 v)永不丢;
|
|
112
|
+
* - detail 只截断不省(先按 300 字口径,再按需收窄,落刀不断占位符);
|
|
113
|
+
* - registryHost / action / 版本按 DIAG_DROP_ORDER 依次省;
|
|
114
|
+
* - 显式 `truncated: true` 当且仅当本次动过 detail 或省过任一键(否则 false)。
|
|
115
|
+
*
|
|
116
|
+
* 纯函数、确定性:同一输入同一输出;不读盘、不联网、不抛错(坏输入回最小骨架)。
|
|
117
|
+
*/
|
|
118
|
+
export declare function enforceDiagBudget<T extends Record<string, unknown>>(input: T): T & {
|
|
119
|
+
truncated: boolean;
|
|
120
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/redaction.ts
|
|
2
|
+
const DETAIL_MAX_CHARS = 300;
|
|
3
|
+
const COPY_BUDGET_CHARS = 1500;
|
|
4
|
+
const DIAG_INLINE_BUDGET_BYTES = 1024;
|
|
5
|
+
const REDACTED_PATH = "<\u8DEF\u5F84>";
|
|
6
|
+
const REDACTED_SECRET = "<\u8131\u654F>";
|
|
7
|
+
const KNOWN_GAP_FILE_URL_PREFIX = "file:///";
|
|
8
|
+
const REDACTION_RULE_NAMES = [
|
|
9
|
+
"R_ABSOLUTE_PATH",
|
|
10
|
+
"R_URL_USERINFO",
|
|
11
|
+
"R_TOKEN",
|
|
12
|
+
"R_CRED_PAIR",
|
|
13
|
+
"R_EMAIL"
|
|
14
|
+
];
|
|
15
|
+
const ENDORSED_REGISTRY_HOSTS = ["registry.npmjs.org"];
|
|
16
|
+
const ABSOLUTE_PATH_RE = /[A-Za-z]:\\[^\s"']*|\\\\[^\s"'()\[\];]+|(^|[\s"'(\[=,])\/\/[^\s"'()\[\];]+|(^|[\s"'(\[=:,])\/(?!\/)[^\s"'()\[\];]+/g;
|
|
17
|
+
const URL_USERINFO_RE = /[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/]*@/;
|
|
18
|
+
const NPM_TOKEN_RE = /\bnpm_[A-Za-z0-9_-]{8,}/g;
|
|
19
|
+
const GITHUB_TOKEN_RE = /\b(?:gh[pousr]_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{10,})/g;
|
|
20
|
+
const SK_TOKEN_RE = /\bsk-[A-Za-z0-9_-]{6,}/g;
|
|
21
|
+
const BEARER_TOKEN_RE = /(\bbearer\s+)[A-Za-z0-9._~+/-=]{6,}/gi;
|
|
22
|
+
const CRED_PAIR_RE = /(\b(?:token|password|passwd|pwd|api[_-]?key|access[_-]?key|auth[_-]?token|secret|cookie)\s*[:=]\s*)("[^"]+"|'[^']+'|[^\s'";,)\]]+)/gi;
|
|
23
|
+
const EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
24
|
+
function flattenWhitespace(text) {
|
|
25
|
+
return String(text ?? "").replace(/\s+/g, " ").trim();
|
|
26
|
+
}
|
|
27
|
+
function applyPathRule(text) {
|
|
28
|
+
ABSOLUTE_PATH_RE.lastIndex = 0;
|
|
29
|
+
return text.replace(ABSOLUTE_PATH_RE, (_whole, g1, g2) => {
|
|
30
|
+
const lead = g1 ?? g2 ?? "";
|
|
31
|
+
return `${lead}${REDACTED_PATH}`;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function applyTokenRule(text) {
|
|
35
|
+
NPM_TOKEN_RE.lastIndex = 0;
|
|
36
|
+
GITHUB_TOKEN_RE.lastIndex = 0;
|
|
37
|
+
SK_TOKEN_RE.lastIndex = 0;
|
|
38
|
+
BEARER_TOKEN_RE.lastIndex = 0;
|
|
39
|
+
return text.replace(NPM_TOKEN_RE, REDACTED_SECRET).replace(GITHUB_TOKEN_RE, REDACTED_SECRET).replace(SK_TOKEN_RE, REDACTED_SECRET).replace(BEARER_TOKEN_RE, (_whole, prefix) => `${prefix}${REDACTED_SECRET}`);
|
|
40
|
+
}
|
|
41
|
+
function applyCredPairRule(text) {
|
|
42
|
+
CRED_PAIR_RE.lastIndex = 0;
|
|
43
|
+
return text.replace(CRED_PAIR_RE, (_whole, head) => `${head}${REDACTED_SECRET}`);
|
|
44
|
+
}
|
|
45
|
+
function applyEmailRule(text) {
|
|
46
|
+
EMAIL_RE.lastIndex = 0;
|
|
47
|
+
return text.replace(EMAIL_RE, REDACTED_SECRET);
|
|
48
|
+
}
|
|
49
|
+
function hasUserinfo(text) {
|
|
50
|
+
URL_USERINFO_RE.lastIndex = 0;
|
|
51
|
+
return URL_USERINFO_RE.test(text);
|
|
52
|
+
}
|
|
53
|
+
function truncateToWordBoundary(text, max) {
|
|
54
|
+
const input = String(text ?? "");
|
|
55
|
+
if (input.length <= max) return input;
|
|
56
|
+
const slice = input.slice(0, max);
|
|
57
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
58
|
+
let cut = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
|
|
59
|
+
const lastOpen = cut.lastIndexOf("<");
|
|
60
|
+
const lastClose = cut.lastIndexOf(">");
|
|
61
|
+
if (lastOpen > lastClose) cut = cut.slice(0, lastOpen).trimEnd();
|
|
62
|
+
if (!cut) cut = slice;
|
|
63
|
+
return `${cut}\u2026`;
|
|
64
|
+
}
|
|
65
|
+
function truncateDetail(text) {
|
|
66
|
+
return truncateToWordBoundary(text, DETAIL_MAX_CHARS);
|
|
67
|
+
}
|
|
68
|
+
function truncateForCopy(text) {
|
|
69
|
+
return truncateToWordBoundary(text, COPY_BUDGET_CHARS);
|
|
70
|
+
}
|
|
71
|
+
function scrubWithoutBudget(text) {
|
|
72
|
+
return applyEmailRule(applyCredPairRule(applyTokenRule(applyPathRule(text))));
|
|
73
|
+
}
|
|
74
|
+
function sanitizeDetail(text) {
|
|
75
|
+
const flat = flattenWhitespace(text);
|
|
76
|
+
if (!flat) return "";
|
|
77
|
+
if (hasUserinfo(flat)) return "";
|
|
78
|
+
return truncateDetail(scrubWithoutBudget(flat));
|
|
79
|
+
}
|
|
80
|
+
function sanitizeForCopy(text) {
|
|
81
|
+
const flat = flattenWhitespace(String(text ?? ""));
|
|
82
|
+
if (!flat) return "";
|
|
83
|
+
if (hasUserinfo(flat)) return "";
|
|
84
|
+
return truncateForCopy(scrubWithoutBudget(flat));
|
|
85
|
+
}
|
|
86
|
+
function resolveRegistryHost(registryUrl) {
|
|
87
|
+
try {
|
|
88
|
+
const raw = String(registryUrl ?? "").trim();
|
|
89
|
+
if (!raw) return null;
|
|
90
|
+
const host = new URL(raw).hostname.toLowerCase();
|
|
91
|
+
if (!host) return null;
|
|
92
|
+
return ENDORSED_REGISTRY_HOSTS.includes(host) ? host : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function diagByteLength(value) {
|
|
98
|
+
try {
|
|
99
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
100
|
+
const encoder = globalThis.TextEncoder;
|
|
101
|
+
if (typeof encoder === "function") return new encoder().encode(text ?? "").length;
|
|
102
|
+
return String(text ?? "").length;
|
|
103
|
+
} catch {
|
|
104
|
+
try {
|
|
105
|
+
return String(JSON.stringify(value) ?? "").length;
|
|
106
|
+
} catch {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const DIAG_NEVER_DROP_KEYS = ["v", "code", "error", "errorKind", "stage", "route", "method", "requestId"];
|
|
112
|
+
const DIAG_DROP_ORDER = [
|
|
113
|
+
"registryHost",
|
|
114
|
+
"action",
|
|
115
|
+
"latestVersion",
|
|
116
|
+
"runningVersion",
|
|
117
|
+
"targetPackageName",
|
|
118
|
+
"latencyMs",
|
|
119
|
+
"exitCode",
|
|
120
|
+
"environmentKind",
|
|
121
|
+
"checkId",
|
|
122
|
+
"httpStatus"
|
|
123
|
+
];
|
|
124
|
+
function isRecord(value) {
|
|
125
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
126
|
+
}
|
|
127
|
+
function enforceDiagBudget(input) {
|
|
128
|
+
const base = isRecord(input) ? { ...input } : {};
|
|
129
|
+
const sizeOf = (obj) => diagByteLength(obj);
|
|
130
|
+
const convergeDetailTo300 = (obj) => {
|
|
131
|
+
const current = obj["detail"];
|
|
132
|
+
if (typeof current !== "string" || !current) return false;
|
|
133
|
+
const next = truncateToWordBoundary(current, DETAIL_MAX_CHARS);
|
|
134
|
+
if (next !== current) {
|
|
135
|
+
obj["detail"] = next;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
};
|
|
140
|
+
const shrinkDetailToFit = (obj) => {
|
|
141
|
+
const current = obj["detail"];
|
|
142
|
+
if (typeof current !== "string" || !current) return false;
|
|
143
|
+
let next = current;
|
|
144
|
+
let changed = false;
|
|
145
|
+
while (next && sizeOf({ ...obj, detail: next, truncated: true }) > DIAG_INLINE_BUDGET_BYTES) {
|
|
146
|
+
const shorter = next.length - 50;
|
|
147
|
+
if (shorter <= 0) {
|
|
148
|
+
next = "";
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const cut = truncateToWordBoundary(next.slice(0, shorter), shorter) || next.slice(0, shorter) + "\u2026";
|
|
152
|
+
if (cut.length >= next.length) {
|
|
153
|
+
next = next.slice(0, shorter) + "\u2026";
|
|
154
|
+
} else {
|
|
155
|
+
next = cut;
|
|
156
|
+
}
|
|
157
|
+
changed = true;
|
|
158
|
+
}
|
|
159
|
+
if (next !== current) {
|
|
160
|
+
obj["detail"] = next;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return changed;
|
|
164
|
+
};
|
|
165
|
+
if (sizeOf({ ...base, truncated: base["truncated"] ?? false }) <= DIAG_INLINE_BUDGET_BYTES) {
|
|
166
|
+
const already = base["truncated"] === true;
|
|
167
|
+
return { ...base, truncated: already };
|
|
168
|
+
}
|
|
169
|
+
const working = { ...base };
|
|
170
|
+
convergeDetailTo300(working);
|
|
171
|
+
for (const key of DIAG_DROP_ORDER) {
|
|
172
|
+
if (sizeOf({ ...working, truncated: true }) <= DIAG_INLINE_BUDGET_BYTES) break;
|
|
173
|
+
if (key in working) {
|
|
174
|
+
delete working[key];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (sizeOf({ ...working, truncated: true }) > DIAG_INLINE_BUDGET_BYTES && typeof working["detail"] === "string" && working["detail"]) {
|
|
178
|
+
shrinkDetailToFit(working);
|
|
179
|
+
if (sizeOf({ ...working, truncated: true }) > DIAG_INLINE_BUDGET_BYTES && typeof working["detail"] === "string" && working["detail"]) {
|
|
180
|
+
working["detail"] = "";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const key of DIAG_NEVER_DROP_KEYS) {
|
|
184
|
+
if (key in base && !(key in working)) working[key] = base[key];
|
|
185
|
+
}
|
|
186
|
+
return { ...working, truncated: true };
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
COPY_BUDGET_CHARS,
|
|
190
|
+
DETAIL_MAX_CHARS,
|
|
191
|
+
DIAG_DROP_ORDER,
|
|
192
|
+
DIAG_INLINE_BUDGET_BYTES,
|
|
193
|
+
DIAG_NEVER_DROP_KEYS,
|
|
194
|
+
ENDORSED_REGISTRY_HOSTS,
|
|
195
|
+
KNOWN_GAP_FILE_URL_PREFIX,
|
|
196
|
+
REDACTED_PATH,
|
|
197
|
+
REDACTED_SECRET,
|
|
198
|
+
REDACTION_RULE_NAMES,
|
|
199
|
+
diagByteLength,
|
|
200
|
+
enforceDiagBudget,
|
|
201
|
+
resolveRegistryHost,
|
|
202
|
+
sanitizeDetail,
|
|
203
|
+
sanitizeForCopy,
|
|
204
|
+
truncateToWordBoundary
|
|
205
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/service.ts — 更新模块的业务流程(安装闭环)。
|
|
3
|
+
*
|
|
4
|
+
* 由 update-core/src/service.ts 原样拎入:只记得规矩,不动手装:查状态读本地、查新版联网问一次、
|
|
5
|
+
* 装前复核用的凭证与指纹在这里生成与比对,安装的排队、去重、复核、备份顺序也在此。
|
|
6
|
+
* 真正的跑腿(读盘、联网、拼命令、执行)全在适配器,本文件零导入,依赖全当参数传。
|
|
7
|
+
*
|
|
8
|
+
* 相对原样的增量(规格 #591 第 4、5 条):目标包名、官方源、联网超时、凭证有效期经 UpdatePorts
|
|
9
|
+
* 的可选字段注入,默认值等于现状常量(冻结默认值)。不传即走现状,老调用零变化。
|
|
10
|
+
*/
|
|
11
|
+
import type { FetchImpl, ReleaseInfo, UpdateCore, UpdateErrorCode, UpdatePorts } from './ports.js';
|
|
12
|
+
export declare const PACKAGE_NAME = "dsh-mattpocock-skills-deck";
|
|
13
|
+
export declare const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
14
|
+
export declare const CHECK_TIMEOUT_MS = 10000;
|
|
15
|
+
export declare const CONFIRMATION_TTL_MS: number;
|
|
16
|
+
/** 2 秒内重复点击复用上次结果,不重新联网。 */
|
|
17
|
+
export declare const RECHECK_WINDOW_MS = 2000;
|
|
18
|
+
export declare const MAX_METADATA_BYTES: number;
|
|
19
|
+
export declare const INTEGRITY_PATTERN = "^sha512-[A-Za-z0-9+/]{86}==$";
|
|
20
|
+
/**
|
|
21
|
+
* 请求编号是否合法(#19 收紧:非空、去空格后 1 到 128 个字符,且为不透明编号形状)。
|
|
22
|
+
*
|
|
23
|
+
* 收紧内容:只收 `[A-Za-z0-9._~-]`(UUID 与 `req-1` 形都在内),拒绝路径 / 令牌 / 邮箱 /
|
|
24
|
+
* 键值对 / URL 用户信息形状——要放进诊断块的正是接入方经公开电话传进来的这个值,
|
|
25
|
+
* 旧口径下路径与令牌形状的字符串都能通过(见 #19 Out of Scope 第 1 件)。
|
|
26
|
+
* 属公开契约变更(破冰范围):兼容说明与迁移口径见 #19 进度区——调用方请用 UUID 或同形状不透明串,
|
|
27
|
+
* 不要把路径、令牌、邮箱、URL 当编号传;旧的合法不透明串(`req-1`、`id-N`、UUID)不受影响。
|
|
28
|
+
*/
|
|
29
|
+
export declare function validRequestId(v: unknown): v is string;
|
|
30
|
+
export declare function updateError(code: UpdateErrorCode): Error & {
|
|
31
|
+
code: UpdateErrorCode;
|
|
32
|
+
};
|
|
33
|
+
export declare function validVersion(v: unknown): v is string;
|
|
34
|
+
/** 版本通道:默认只推 stable;prerelease 须调用方显式 opt-in(#16)。 */
|
|
35
|
+
export type ReleaseChannel = 'stable' | 'prerelease';
|
|
36
|
+
export declare const RELEASE_CHANNELS: readonly ["stable", "prerelease"];
|
|
37
|
+
export declare function validReleaseChannel(v: unknown): v is ReleaseChannel;
|
|
38
|
+
/**
|
|
39
|
+
* 发行版本号(含预发布):`X.Y.Z` 或 `X.Y.Z-ids`(ids 见 SemVer §9)。
|
|
40
|
+
* stable 版(纯三段)恒为真;`0.2.0-rc.2` 这类预发布在此为真、在 validVersion 为假。
|
|
41
|
+
*/
|
|
42
|
+
export declare function validReleaseVersion(v: unknown): v is string;
|
|
43
|
+
/** 是否预发布版本(发行合法但非纯三段)。 */
|
|
44
|
+
export declare function isPrereleaseVersion(v: unknown): v is string;
|
|
45
|
+
/** 该版本在该通道是否允许出现:stable 通道只收纯三段,prerelease 通道两者皆收。 */
|
|
46
|
+
export declare function isVersionAllowedInChannel(version: unknown, channel?: ReleaseChannel): version is string;
|
|
47
|
+
/** -1 更小,0 相等,1 更大(入参须先过 validVersion 或 parseTriple)。 */
|
|
48
|
+
export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
49
|
+
/**
|
|
50
|
+
* 发行版比较(SemVer §11):先比三段数字;三段相等时,无预发布 > 有预发布;
|
|
51
|
+
* 都有预发布则按标识符逐个比(纯数字按数字比、数字 < 字母、段多者大)。
|
|
52
|
+
* 入参须先过 validReleaseVersion,否则抛 invalid-release。
|
|
53
|
+
*/
|
|
54
|
+
export declare function compareReleaseVersions(a: string, b: string): -1 | 0 | 1;
|
|
55
|
+
/**
|
|
56
|
+
* 运行环境是否满足远端要求的 Node 范围。
|
|
57
|
+
* 只看懂星号、精确版与 ^ ~ > >= < <= 比较式;看不懂的写法一律放行不误拦。
|
|
58
|
+
*/
|
|
59
|
+
export declare function satisfiesNodeRange(nodeVersion: string, range: string | undefined): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 问官方源的最新版接口,只接受名字对得上、版本号合法正式版、
|
|
62
|
+
* 包地址与完整性校验全合规的返回,否则按版本信息无效处理。
|
|
63
|
+
*
|
|
64
|
+
* 联网取数失败带 httpStatus(仅 !ok 有;假件不给即省略,diag 按阶段×键表断言):
|
|
65
|
+
* 取数阶段该有、校验阶段不该有(由 diag 层按阶段强制省略,本层只诚实携带)。
|
|
66
|
+
*/
|
|
67
|
+
export declare function fetchNpmRelease(fetchImpl: FetchImpl, timeoutMs?: number, opts?: {
|
|
68
|
+
targetPackageName?: string;
|
|
69
|
+
registryUrl?: string;
|
|
70
|
+
releaseChannel?: ReleaseChannel;
|
|
71
|
+
}): Promise<ReleaseInfo>;
|
|
72
|
+
export declare function createUpdateCore(ports: UpdatePorts): UpdateCore;
|
package/dist/service.js
CHANGED
|
@@ -7,7 +7,12 @@ const RECHECK_WINDOW_MS = 2e3;
|
|
|
7
7
|
const MAX_METADATA_BYTES = 256 * 1024;
|
|
8
8
|
const INTEGRITY_PATTERN = "^sha512-[A-Za-z0-9+/]{86}==$";
|
|
9
9
|
function validRequestId(v) {
|
|
10
|
-
|
|
10
|
+
if (typeof v !== "string") return false;
|
|
11
|
+
const id = v.trim();
|
|
12
|
+
if (id.length < 1 || id.length > 128) return false;
|
|
13
|
+
if (!/^[A-Za-z0-9._~-]+$/.test(id)) return false;
|
|
14
|
+
if (/^(npm_|gh[pousr]_|github_pat_|sk-|bearer)/i.test(id)) return false;
|
|
15
|
+
return true;
|
|
11
16
|
}
|
|
12
17
|
function updateError(code) {
|
|
13
18
|
return Object.assign(new Error(code), { code });
|
|
@@ -15,6 +20,37 @@ function updateError(code) {
|
|
|
15
20
|
function validVersion(v) {
|
|
16
21
|
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
|
|
17
22
|
}
|
|
23
|
+
const RELEASE_CHANNELS = ["stable", "prerelease"];
|
|
24
|
+
function validReleaseChannel(v) {
|
|
25
|
+
return v === "stable" || v === "prerelease";
|
|
26
|
+
}
|
|
27
|
+
function validPrereleaseIds(ids) {
|
|
28
|
+
if (typeof ids !== "string" || !ids) return false;
|
|
29
|
+
const parts = ids.split(".");
|
|
30
|
+
if (parts.length === 0) return false;
|
|
31
|
+
for (const p of parts) {
|
|
32
|
+
if (!p || !/^[0-9A-Za-z-]+$/.test(p)) return false;
|
|
33
|
+
if (/^\d+$/.test(p) && p.length > 1 && p.startsWith("0")) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function validReleaseVersion(v) {
|
|
38
|
+
if (typeof v !== "string" || !v) return false;
|
|
39
|
+
const dash = v.indexOf("-");
|
|
40
|
+
if (dash < 0) return validVersion(v);
|
|
41
|
+
const core = v.slice(0, dash);
|
|
42
|
+
const ids = v.slice(dash + 1);
|
|
43
|
+
if (!validVersion(core) || !validPrereleaseIds(ids)) return false;
|
|
44
|
+
if (ids.includes("+")) return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function isPrereleaseVersion(v) {
|
|
48
|
+
return validReleaseVersion(v) && !validVersion(v);
|
|
49
|
+
}
|
|
50
|
+
function isVersionAllowedInChannel(version, channel = "stable") {
|
|
51
|
+
if (channel === "prerelease") return validReleaseVersion(version);
|
|
52
|
+
return validVersion(version);
|
|
53
|
+
}
|
|
18
54
|
function parseTriple(v) {
|
|
19
55
|
const parts = String(v).split(".");
|
|
20
56
|
if (parts.length > 3) return null;
|
|
@@ -38,6 +74,40 @@ function compareVersions(a, b) {
|
|
|
38
74
|
}
|
|
39
75
|
return 0;
|
|
40
76
|
}
|
|
77
|
+
function parseReleaseIds(ids) {
|
|
78
|
+
return ids.split(".").map((p) => /^\d+$/.test(p) ? Number(p) : p);
|
|
79
|
+
}
|
|
80
|
+
function compareReleaseVersions(a, b) {
|
|
81
|
+
if (!validReleaseVersion(a) || !validReleaseVersion(b)) throw updateError("invalid-release");
|
|
82
|
+
const dashA = String(a).indexOf("-");
|
|
83
|
+
const dashB = String(b).indexOf("-");
|
|
84
|
+
const coreA = dashA < 0 ? String(a) : String(a).slice(0, dashA);
|
|
85
|
+
const coreB = dashB < 0 ? String(b) : String(b).slice(0, dashB);
|
|
86
|
+
const order = compareVersions(coreA, coreB);
|
|
87
|
+
if (order !== 0) return order;
|
|
88
|
+
const preA = dashA < 0 ? null : parseReleaseIds(String(a).slice(dashA + 1));
|
|
89
|
+
const preB = dashB < 0 ? null : parseReleaseIds(String(b).slice(dashB + 1));
|
|
90
|
+
if (preA === null && preB === null) return 0;
|
|
91
|
+
if (preA === null) return 1;
|
|
92
|
+
if (preB === null) return -1;
|
|
93
|
+
const width = Math.max(preA.length, preB.length);
|
|
94
|
+
for (let i = 0; i < width; i++) {
|
|
95
|
+
const x = preA[i];
|
|
96
|
+
const y = preB[i];
|
|
97
|
+
if (x === void 0) return -1;
|
|
98
|
+
if (y === void 0) return 1;
|
|
99
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
100
|
+
if (x < y) return -1;
|
|
101
|
+
if (x > y) return 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (typeof x === "number") return -1;
|
|
105
|
+
if (typeof y === "number") return 1;
|
|
106
|
+
if (x < y) return -1;
|
|
107
|
+
if (x > y) return 1;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
41
111
|
function compareOne(node, op, target) {
|
|
42
112
|
const order = compareTriple(node, target);
|
|
43
113
|
if (op === "=" || op === "") return order === 0;
|
|
@@ -102,9 +172,20 @@ function timeoutSignal(ms) {
|
|
|
102
172
|
}
|
|
103
173
|
return void 0;
|
|
104
174
|
}
|
|
175
|
+
function httpStatusOf(response) {
|
|
176
|
+
const raw = response?.status;
|
|
177
|
+
return typeof raw === "number" && Number.isInteger(raw) && raw >= 100 && raw <= 599 ? raw : null;
|
|
178
|
+
}
|
|
179
|
+
function checkFailedWithStatus(response) {
|
|
180
|
+
const err = updateError("check-failed");
|
|
181
|
+
const status = httpStatusOf(response);
|
|
182
|
+
if (status !== null) err.httpStatus = status;
|
|
183
|
+
return err;
|
|
184
|
+
}
|
|
105
185
|
async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
106
186
|
const targetName = opts?.targetPackageName ?? PACKAGE_NAME;
|
|
107
187
|
const registry = opts?.registryUrl ?? NPM_REGISTRY;
|
|
188
|
+
const channel = opts?.releaseChannel === "prerelease" ? "prerelease" : "stable";
|
|
108
189
|
let response = null;
|
|
109
190
|
try {
|
|
110
191
|
response = await fetchImpl(`${registry}${encodeURIComponent(targetName)}/latest`, {
|
|
@@ -116,13 +197,13 @@ async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
|
116
197
|
throw updateError("check-failed");
|
|
117
198
|
}
|
|
118
199
|
try {
|
|
119
|
-
if (!response.ok) throw
|
|
200
|
+
if (!response.ok) throw checkFailedWithStatus(response);
|
|
120
201
|
const declared = Number(response.headers.get("content-length"));
|
|
121
202
|
if (Number.isFinite(declared) && declared > MAX_METADATA_BYTES) throw updateError("invalid-release");
|
|
122
203
|
const text = await response.text();
|
|
123
204
|
if (byteLength(text) > MAX_METADATA_BYTES) throw updateError("invalid-release");
|
|
124
205
|
const value = JSON.parse(text);
|
|
125
|
-
if (value.name !== targetName || !
|
|
206
|
+
if (value.name !== targetName || !isVersionAllowedInChannel(value.version, channel)) throw updateError("invalid-release");
|
|
126
207
|
const version = value.version;
|
|
127
208
|
const nodeRange = value.engines?.node;
|
|
128
209
|
if (nodeRange !== void 0 && typeof nodeRange !== "string") throw updateError("invalid-release");
|
|
@@ -142,7 +223,7 @@ async function fetchNpmRelease(fetchImpl, timeoutMs = CHECK_TIMEOUT_MS, opts) {
|
|
|
142
223
|
if (error?.code === "check-failed") throw error;
|
|
143
224
|
if (error?.code === "invalid-release") throw error;
|
|
144
225
|
if (response !== null && response.ok) throw updateError("invalid-release");
|
|
145
|
-
throw
|
|
226
|
+
throw checkFailedWithStatus(response);
|
|
146
227
|
}
|
|
147
228
|
}
|
|
148
229
|
function createUpdateCore(ports) {
|
|
@@ -150,8 +231,9 @@ function createUpdateCore(ports) {
|
|
|
150
231
|
const confirmationTtlMs = ports.confirmationTtlMs ?? CONFIRMATION_TTL_MS;
|
|
151
232
|
const targetPackageName = ports.targetPackageName ?? PACKAGE_NAME;
|
|
152
233
|
const registryUrl = ports.registryUrl ?? NPM_REGISTRY;
|
|
234
|
+
const releaseChannel = ports.releaseChannel === "prerelease" ? "prerelease" : "stable";
|
|
153
235
|
function fetchRelease() {
|
|
154
|
-
return fetchNpmRelease(ports.fetchImpl, checkTimeoutMs, { targetPackageName, registryUrl });
|
|
236
|
+
return fetchNpmRelease(ports.fetchImpl, checkTimeoutMs, { targetPackageName, registryUrl, releaseChannel });
|
|
155
237
|
}
|
|
156
238
|
let checked = null;
|
|
157
239
|
let checking = null;
|
|
@@ -215,8 +297,14 @@ function createUpdateCore(ports) {
|
|
|
215
297
|
const busy = job?.state === "installing" || job?.state === "verifying";
|
|
216
298
|
const fresh = checked !== null && checked.checkId !== null && ports.now() < checked.expiresAt;
|
|
217
299
|
const runningVersion = ports.readRunningVersion();
|
|
300
|
+
let newer = false;
|
|
301
|
+
try {
|
|
302
|
+
newer = isVersionAllowedInChannel(runningVersion, releaseChannel) && !!checked?.release && isVersionAllowedInChannel(checked.release.version, releaseChannel) && compareReleaseVersions(checked.release.version, runningVersion) === 1;
|
|
303
|
+
} catch {
|
|
304
|
+
newer = false;
|
|
305
|
+
}
|
|
218
306
|
const canInstall = Boolean(
|
|
219
|
-
env.eligible && !blockedReason && !busy && fresh && checked?.installationKey === env.installationKey &&
|
|
307
|
+
env.eligible && !blockedReason && !busy && fresh && checked?.installationKey === env.installationKey && newer
|
|
220
308
|
);
|
|
221
309
|
return {
|
|
222
310
|
runningVersion,
|
|
@@ -353,11 +441,17 @@ export {
|
|
|
353
441
|
NPM_REGISTRY,
|
|
354
442
|
PACKAGE_NAME,
|
|
355
443
|
RECHECK_WINDOW_MS,
|
|
444
|
+
RELEASE_CHANNELS,
|
|
445
|
+
compareReleaseVersions,
|
|
356
446
|
compareVersions,
|
|
357
447
|
createUpdateCore,
|
|
358
448
|
fetchNpmRelease,
|
|
449
|
+
isPrereleaseVersion,
|
|
450
|
+
isVersionAllowedInChannel,
|
|
359
451
|
satisfiesNodeRange,
|
|
360
452
|
updateError,
|
|
453
|
+
validReleaseChannel,
|
|
454
|
+
validReleaseVersion,
|
|
361
455
|
validRequestId,
|
|
362
456
|
validVersion
|
|
363
457
|
};
|