dsh-offpeak-guard 0.3.2 → 0.3.3
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 +8 -0
- package/README.en.md +9 -0
- package/README.md +9 -0
- package/index.js +53 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3 — 2026-11-14
|
|
4
|
+
|
|
5
|
+
- 设置接口 `/plugins/dsh-offpeak-guard/action` 补上准入:路由只在 `webServer` 与 `connection` 两个服务都在位时注册,每个请求先过 `connection.requestRejection(req)`(Host/Origin 校验 + 浏览器令牌鉴权),返回 401/403 直接拒绝。此前本机任意网页都能改会话豁免、峰谷规则和暂停方式。
|
|
6
|
+
- 拿不到 `connection` 或它抛错时一律按拒绝处理(失败关闭),不退回到只检查 Host/Origin/回环地址。
|
|
7
|
+
- 请求体加 64 KiB 上限,超出返回 413;其余错误仍返回 400。
|
|
8
|
+
- 新增 `test/admission.test.mjs`:覆盖缺服务、读取抛错、`requestRejection` 抛错、放行与 401/403 透传五条路径。
|
|
9
|
+
- 适配 DSH 0.2.2-rc.2 的 `@deepseek-ai/dsh-client-connection` 服务约定(`requestRejection` 接收 `{ headers }`)。
|
|
10
|
+
|
|
3
11
|
## 0.3.2 — 2026-11-06
|
|
4
12
|
|
|
5
13
|
- 修复设置面板切换峰时策略时报 `Unexpected end of JSON input`:Host 启动顺序可能让 `webServer` 在插件 apply 时尚未就绪,导致设置路由根本没注册;改用 `ctx.inject(['webServer'], ...)` 后再挂载路由。
|
package/README.en.md
CHANGED
|
@@ -110,6 +110,15 @@ Click the sidebar clock to open it:
|
|
|
110
110
|
- **Session exemptions**: searchable list of live root sessions; hover a session row in the left sidebar for a shield button, or use the **Exempt this session** button in the composer strip to toggle the current conversation directly;
|
|
111
111
|
- **Holidays**: show the dynamic sync state.
|
|
112
112
|
|
|
113
|
+
### Route admission
|
|
114
|
+
|
|
115
|
+
The settings route `/plugins/dsh-offpeak-guard/action` goes through host authentication:
|
|
116
|
+
|
|
117
|
+
- the route is only registered once both `webServer` and `connection` are available;
|
|
118
|
+
- every request passes `connection.requestRejection(req)` first — its Host/Origin fence stops DNS rebinding and cross-site calls, and its browser authentication stops pages that never logged in; a 401/403 is answered before any business logic runs;
|
|
119
|
+
- a missing `connection`, or one that throws, rejects the request (fail closed); there is no fallback to checking only Host, Origin or the loopback address;
|
|
120
|
+
- request bodies are capped at 64 KiB and answer 413 above that.
|
|
121
|
+
|
|
113
122
|
## Subagents and waiting
|
|
114
123
|
|
|
115
124
|
- Running subagents are allowed to finish; the parent is paused after the last child ends and resumed off-peak;
|
package/README.md
CHANGED
|
@@ -112,6 +112,15 @@
|
|
|
112
112
|
|
|
113
113
|
面板里的修改立即生效并持久化。
|
|
114
114
|
|
|
115
|
+
### 接口准入
|
|
116
|
+
|
|
117
|
+
设置接口 `/plugins/dsh-offpeak-guard/action` 走宿主认证,不对外开放:
|
|
118
|
+
|
|
119
|
+
- 路由在 `webServer` 与 `connection` 两个服务都在位时才注册;
|
|
120
|
+
- 每个请求先过 `connection.requestRejection(req)`——Host/Origin 校验挡住 DNS rebinding 与跨站调用,浏览器令牌鉴权挡住没登录的本机网页;返回 401/403 时直接拒绝,不进入业务逻辑;
|
|
121
|
+
- 拿不到 `connection` 或它抛错时一律按拒绝处理(失败关闭),不会退回只检查 Host、Origin 或回环地址;
|
|
122
|
+
- 请求体上限 64 KiB,超出返回 413。
|
|
123
|
+
|
|
115
124
|
## 子代理与等待
|
|
116
125
|
|
|
117
126
|
- 峰时开始时若会话有**正在运行的子代理**,先放行父会话与子代理;
|
package/index.js
CHANGED
|
@@ -149,11 +149,25 @@ export function envConfig(env = process.env) {
|
|
|
149
149
|
return out;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** 设置接口只收几个字段的小 JSON,超过这个体积按可疑请求拒绝。 */
|
|
153
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
154
|
+
|
|
152
155
|
async function readJsonBody(req) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
const chunks = [];
|
|
157
|
+
let size = 0;
|
|
158
|
+
for await (const chunk of req) {
|
|
159
|
+
size += chunk?.byteLength ?? Buffer.byteLength(String(chunk));
|
|
160
|
+
if (size > MAX_BODY_BYTES) {
|
|
161
|
+
// 先排空再拒绝,让客户端读到响应而不是被一刀切断连接。
|
|
162
|
+
req.resume();
|
|
163
|
+
const error = new Error('request body too large');
|
|
164
|
+
error.statusCode = 413;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
chunks.push(chunk);
|
|
168
|
+
}
|
|
169
|
+
if (size === 0) return {};
|
|
170
|
+
return JSON.parse(Buffer.concat(chunks, size).toString('utf8'));
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
function sendJson(res, status, payload) {
|
|
@@ -164,6 +178,33 @@ function sendJson(res, status, payload) {
|
|
|
164
178
|
res.end(body);
|
|
165
179
|
}
|
|
166
180
|
|
|
181
|
+
/**
|
|
182
|
+
* 设置路由的准入栅栏。
|
|
183
|
+
*
|
|
184
|
+
* `connection.requestRejection` 是宿主唯一的本机接口准入点:它同时做 Host/Origin
|
|
185
|
+
* 校验(挡 DNS rebinding 和跨站请求)与浏览器令牌鉴权(挡没登录的本机网页)。
|
|
186
|
+
* 没有它,本机任意网页都能改会话豁免、峰谷规则和暂停方式。
|
|
187
|
+
*
|
|
188
|
+
* 拿不到 `connection`、或它自己抛错时一律拒绝(失败关闭),不退回到只检查
|
|
189
|
+
* Host/Origin/回环地址——那条路挡不住别的网页。
|
|
190
|
+
*/
|
|
191
|
+
export function admissionRejection(ctx, req) {
|
|
192
|
+
let connection;
|
|
193
|
+
try {
|
|
194
|
+
connection = Reflect.get(ctx, 'connection');
|
|
195
|
+
} catch {
|
|
196
|
+
return 403;
|
|
197
|
+
}
|
|
198
|
+
if (connection === undefined || connection === null || typeof connection.requestRejection !== 'function') {
|
|
199
|
+
return 403;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return connection.requestRejection({ headers: req.headers }) ?? undefined;
|
|
203
|
+
} catch {
|
|
204
|
+
return 403;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
167
208
|
export function apply(ctx, rawConfig = {}) {
|
|
168
209
|
const config = { ...(rawConfig ?? {}), ...envConfig() };
|
|
169
210
|
const guard = createGuard(ctx, config);
|
|
@@ -197,7 +238,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
197
238
|
};
|
|
198
239
|
}, 'offpeak-guard: tools');
|
|
199
240
|
|
|
200
|
-
ctx.inject(['webServer'], (webCtx) => {
|
|
241
|
+
ctx.inject(['webServer', 'connection'], (webCtx) => {
|
|
201
242
|
webCtx.effect(
|
|
202
243
|
() =>
|
|
203
244
|
webCtx.webServer.register({
|
|
@@ -205,6 +246,11 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
205
246
|
path: '/plugins/dsh-offpeak-guard',
|
|
206
247
|
async handler(req, res) {
|
|
207
248
|
try {
|
|
249
|
+
const rejection = admissionRejection(webCtx, req);
|
|
250
|
+
if (rejection !== undefined) {
|
|
251
|
+
sendJson(res, rejection, { ok: false, error: 'unauthorized' });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
208
254
|
const url = new URL(req.url || '/', 'http://x');
|
|
209
255
|
const rest = url.pathname.replace(/^\/plugins\/dsh-offpeak-guard/, '') || '/';
|
|
210
256
|
if (req.method !== 'POST' || rest !== '/action') {
|
|
@@ -234,7 +280,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
234
280
|
}
|
|
235
281
|
sendJson(res, 400, { ok: false, error: 'unknown action' });
|
|
236
282
|
} catch (error) {
|
|
237
|
-
|
|
283
|
+
const status = Number.isInteger(error?.statusCode) ? error.statusCode : 400;
|
|
284
|
+
sendJson(res, status, { ok: false, error: String(error?.message ?? error) });
|
|
238
285
|
}
|
|
239
286
|
},
|
|
240
287
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-offpeak-guard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "DeepSeek peak/off-peak pricing guard for DSH: detects the price window and current model, pauses running tasks at peak, resumes them automatically when off-peak begins, and schedules work for the next off-peak window.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|