@xrn1997/dsh-session-delete 0.5.0 → 0.6.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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 宿主 `attachments` 服务的窄镜像(ADR-0035):只声明本插件读取附件用到的两条。
3
+ *
4
+ * - 服务是**可选**的:`ctx.get('attachments')` 每次调用现查(服务可能晚于插件激活),
5
+ * `available()` 为假时调用方退回旧的直接 fs 路径。
6
+ * - `readImage` 会按记录里的引用**校验字节**;`readFileStream` 校验并分块读出。
7
+ * 两者都不在本文件里重写校验逻辑。
8
+ * - 不 import 宿主 `attachments` 服务的类型包:形状按该服务的运行期契约抄成窄镜像。
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ export type ImageMediaTypeLike = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
12
+ export interface ImageAttachmentRefLike {
13
+ readonly attachmentId: string;
14
+ readonly mediaType: ImageMediaTypeLike;
15
+ readonly bytes: number;
16
+ readonly width: number;
17
+ readonly height: number;
18
+ readonly name?: string;
19
+ }
20
+ export interface FileAttachmentRefLike {
21
+ readonly attachmentId: string;
22
+ readonly name: string;
23
+ readonly bytes: number;
24
+ }
25
+ export interface StoredImageAttachmentLike {
26
+ readonly ref: ImageAttachmentRefLike;
27
+ readonly data: Uint8Array;
28
+ }
29
+ export interface AttachmentsPort {
30
+ /** 服务在场且成员形状可用;调用方用它决定走服务还是直接 fs。 */
31
+ available(): boolean;
32
+ readImage(ref: ImageAttachmentRefLike): Promise<StoredImageAttachmentLike>;
33
+ readFileStream(ref: FileAttachmentRefLike): AsyncIterable<Uint8Array>;
34
+ }
35
+ /** 服务在 `available()` 之后消失/形状变了:调用方可以退回直接 fs,而不是把这次读取判成 io。 */
36
+ export declare class AttachmentsUnavailableError extends Error {
37
+ constructor(message?: string);
38
+ }
39
+ /** 每次调用惰性解析服务:插件与 `attachments` 的激活顺序没有承诺。 */
40
+ export declare function createAttachmentsPort(ctx: Context): AttachmentsPort;
@@ -6,13 +6,18 @@
6
6
  * 信封 {ok:true,value} / {ok:false,error:{code,message,details?}} 也是两半的共同契约。
7
7
  */
8
8
  /**
9
- * 插件专属前缀。
9
+ * 插件专属前缀(九段相对它命名)。
10
10
  *
11
- * 为什么必须带插件名:宿主 web server 的 `register({kind:'prefix', path})` 对**重复的 (kind, path)
12
- * 直接抛错**(`dsh-host-webserver` 原文 `webserver: duplicate ${route.kind} route "${route.path}"`),
13
- * 撞了就是整棵插件树 boot 失败——所以这条路径是与生态的硬边界,不是显示好不好看的问题。
11
+ * 为什么必须带插件名:宿主 `connection.fetch.register` 对**重复的精确路径**直接抛错
12
+ * (`connection: exact Fetch route "…" is already registered`),撞了就是整棵插件树 boot 失败——
13
+ * 所以这条路径是与生态的硬边界,不是显示好不好看的问题。
14
14
  */
15
15
  export declare const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
16
+ /**
17
+ * 唯一的取数基础路径:宿主 `/api` + 插件前缀。九段 exact Fetch 路由、客户端探测与附件 URL
18
+ * 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
19
+ */
20
+ export declare const OFFICIAL_SESSION_DELETE_API_PREFIX = "/api/dsh-session-delete-api";
16
21
  /** 九个段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
17
22
  * (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
18
23
  * 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
@@ -251,7 +256,8 @@ export interface AttachmentOrphanItemWire {
251
256
  bytes: number;
252
257
  }
253
258
  /** 孤儿附件报告(`GET /attachment-orphans`):数量、总字节与对象清单。
254
- * 清单只带 id/bytes(不含路径/文件名),供设置页预览缩略图,最多 200 条;数量与字节按全量算。
259
+ * 清单只带 id/bytes(不含路径/文件名),供设置页预览缩略图;条数由 Node 半配置
260
+ * (`src/config.ts` 的 `orphanPreviewLimit`,默认 200),数量与字节按全量算。
255
261
  * 读不出任何一处引用源时**不返回这一形状**,而是 `io` 失败——不拿 0 冒充"没有孤儿"。 */
256
262
  export interface AttachmentOrphanReportWire {
257
263
  count: number;
@@ -297,6 +303,13 @@ export declare const PARAMS: {
297
303
  readonly attachmentId: "id";
298
304
  readonly attachmentKind: "kind";
299
305
  readonly attachmentName: "name";
306
+ /** 图片引用的媒体类型 / 宽高:只给宿主 `attachments.readImage` 做引用校验,不参与路径拼接。 */
307
+ readonly attachmentMediaType: "mediaType";
308
+ /** 字节数:图片支与文件支都带——它是宿主读面的引用一部分,并先过 `maxAttachmentBytes` 闸
309
+ * (别为一张已知超限的图让宿主整份读出来)。 */
310
+ readonly attachmentBytes: "bytes";
311
+ readonly attachmentWidth: "width";
312
+ readonly attachmentHeight: "height";
300
313
  };
301
314
  /**
302
315
  * 失败信封的**线上形状**(本文件):`code` 是判别依据、`message` 是上屏原文、
@@ -1,4 +1,5 @@
1
1
  import type { FsPort } from '../ports/fs-port.js';
2
+ import type { SessionDeleteConfig } from '../config.js';
2
3
  import { type AttachmentCleanupWire } from '../shared/wire.js';
3
4
  export interface ImageRef {
4
5
  kind: 'image';
@@ -30,6 +31,7 @@ export declare function attachmentsInContent(content: unknown): AttachmentRef[];
30
31
  export interface AttachmentReadDeps {
31
32
  fs: Pick<FsPort, 'exists' | 'listFiles' | 'listDirs' | 'readFile' | 'statFile' | 'realpath'>;
32
33
  home: string;
34
+ config: Pick<SessionDeleteConfig, 'maxJsonRefBytes'>;
33
35
  }
34
36
  export interface AttachmentRemovalDeps extends AttachmentReadDeps {
35
37
  fs: Pick<FsPort, 'exists' | 'listFiles' | 'listDirs' | 'readFile' | 'statFile' | 'realpath' | 'removeFile'>;
@@ -27,8 +27,9 @@ interface RawGroup {
27
27
  * 彻底删除(组语义见 `src/trash/groups.ts`),把它的根当孤儿删掉就是**替用户做了一个没人点过的决定**。孤儿只
28
28
  * 有一种来路:父被彻底删掉了(或被手工从盘上清掉了),清册里再也没有这一行。
29
29
  *
30
- * 根按**传递闭包**收全部后代(与 `HostCorpusPort.subagentMembersOf` 同一实现:闭包核心住
31
- * `src/domain/closure.ts` 的 `orphanRootsOf`,孩子与孙代都算,深度不设上限),同一条记录只归一组
30
+ * 根按**传递闭包**收全部 subagent 后代(与 `HostCorpusPort.subagentMembersOf` 同一实现:闭包核心住
31
+ * `src/domain/closure.ts` 的 `orphanRootsOf`,孩子与孙代都算,深度不设上限;普通 fork 不是成员、
32
+ * 也不穿过它收后代),同一条记录只归一组
32
33
  * (`claimed`)——数据里真出现环时结果是"少数一条",不是把一个会话删两遍。
33
34
  * 归堆与去重**两边都去 `session-` 前缀**:同一个父在两代宿主里写法不同(见 `bareSessionId` 的来历)。
34
35
  */
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "Session Delete",
4
- "description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "Session Delete",
4
+ "description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
5
+ }
6
+ }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "会话删除",
4
- "description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "会话删除",
4
+ "description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,117 +1,117 @@
1
- {
2
- "name": "@xrn1997/dsh-session-delete",
3
- "version": "0.5.0",
4
- "description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
5
- "type": "module",
6
- "packageManager": "pnpm@11.25.0",
7
- "main": "lib/index.js",
8
- "types": "lib/types/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./lib/types/index.d.ts",
12
- "default": "./lib/index.js"
13
- },
14
- "./client": {
15
- "types": "./lib/types/client/index.d.ts",
16
- "default": "./lib/client.js"
17
- },
18
- "./locale/*.json": "./locale/*.json",
19
- "./package.json": "./package.json"
20
- },
21
- "icon": "./icon.svg",
22
- "files": [
23
- "lib/index.js",
24
- "lib/client.js",
25
- "lib/types/**/*.d.ts",
26
- "locale",
27
- "icon.svg",
28
- "cordis.patch.yml",
29
- "README.md",
30
- "LICENSE"
31
- ],
32
- "engines": {
33
- "node": ">=24",
34
- "dsh": ">=0.1.7-rc.2 <0.3.0-0"
35
- },
36
- "scripts": {
37
- "build": "tsdown && tsc -p tsconfig.build.json",
38
- "typecheck": "tsc --noEmit",
39
- "lint": "eslint .",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "pack:dry": "npm pack --dry-run --ignore-scripts",
43
- "prepack": "npm run build",
44
- "gates": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm run pack:dry"
45
- },
46
- "peerDependencies": {
47
- "@deepseek-ai/cordis": "^4.0.4",
48
- "react": "^18.3.1"
49
- },
50
- "devDependencies": {
51
- "@deepseek-ai/cordis": "^4.0.4",
52
- "@eslint/js": "^10.0.1",
53
- "@testing-library/dom": "^10.4.2",
54
- "@testing-library/jest-dom": "^6.10.0",
55
- "@testing-library/react": "^16.3.3",
56
- "@types/node": "24.19.0",
57
- "@types/react": "18.3.31",
58
- "@types/react-dom": "18.3.5",
59
- "@types/semver": "^7.8.0",
60
- "eslint": "^10.14.0",
61
- "eslint-plugin-react-hooks": "^7.1.1",
62
- "globals": "^17.13.0",
63
- "jsdom": "^26.1.0",
64
- "react": "^18.3.1",
65
- "react-dom": "18.3.1",
66
- "semver": "^7.8.5",
67
- "tsdown": "0.23.0",
68
- "typescript": "5.9.3",
69
- "typescript-eslint": "^8.72.1",
70
- "vitest": "3.2.7"
71
- },
72
- "dsh": {
73
- "bundle": {
74
- "patch": "./cordis.patch.yml"
75
- },
76
- "client": {
77
- "inject": [
78
- "@deepseek-ai/dsh-client-locale"
79
- ],
80
- "platform": "web"
81
- },
82
- "compatibility": {
83
- "dshReleases": {
84
- "0.2.0-rc.2": "compatible",
85
- "0.2.1-rc.1": "compatible",
86
- "0.2.1-rc.2": "compatible",
87
- "0.2.1-rc.3": "compatible",
88
- "0.2.1-rc.5": "compatible"
89
- }
90
- }
91
- },
92
- "publishConfig": {
93
- "access": "public"
94
- },
95
- "repository": {
96
- "type": "git",
97
- "url": "git+https://github.com/xrn1997/dsh-session-delete.git"
98
- },
99
- "homepage": "https://github.com/xrn1997/dsh-session-delete#readme",
100
- "bugs": {
101
- "url": "https://github.com/xrn1997/dsh-session-delete/issues"
102
- },
103
- "keywords": [
104
- "dsh",
105
- "deepseek-harness",
106
- "plugin",
107
- "session",
108
- "trash",
109
- "delete"
110
- ],
111
- "author": "xrn1997",
112
- "license": "Apache-2.0",
113
- "dependencies": {
114
- "react-markdown": "^10.1.0",
115
- "remark-gfm": "^4.0.1"
116
- }
117
- }
1
+ {
2
+ "name": "@xrn1997/dsh-session-delete",
3
+ "version": "0.6.0",
4
+ "description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.25.0",
7
+ "main": "lib/index.js",
8
+ "types": "lib/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/types/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./client": {
15
+ "types": "./lib/types/client/index.d.ts",
16
+ "default": "./lib/client.js"
17
+ },
18
+ "./locale/*.json": "./locale/*.json",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "icon": "./icon.svg",
22
+ "files": [
23
+ "lib/index.js",
24
+ "lib/client.js",
25
+ "lib/types/**/*.d.ts",
26
+ "locale",
27
+ "icon.svg",
28
+ "cordis.patch.yml",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=24",
34
+ "dsh": ">=0.1.7-rc.2 <0.3.0-0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsdown && tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc --noEmit",
39
+ "lint": "eslint .",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "pack:dry": "npm pack --dry-run --ignore-scripts",
43
+ "prepack": "npm run build",
44
+ "gates": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm run pack:dry"
45
+ },
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "^4.0.4",
48
+ "react": "^18.3.1"
49
+ },
50
+ "devDependencies": {
51
+ "@deepseek-ai/cordis": "^4.0.4",
52
+ "@eslint/js": "^10.0.1",
53
+ "@testing-library/dom": "^10.4.2",
54
+ "@testing-library/jest-dom": "^6.10.0",
55
+ "@testing-library/react": "^16.3.3",
56
+ "@types/node": "24.19.0",
57
+ "@types/react": "18.3.31",
58
+ "@types/react-dom": "18.3.5",
59
+ "@types/semver": "^7.8.0",
60
+ "eslint": "^10.14.0",
61
+ "eslint-plugin-react-hooks": "^7.1.1",
62
+ "globals": "^17.13.0",
63
+ "jsdom": "^26.1.0",
64
+ "react": "^18.3.1",
65
+ "react-dom": "18.3.1",
66
+ "semver": "^7.8.5",
67
+ "tsdown": "0.23.0",
68
+ "typescript": "5.9.3",
69
+ "typescript-eslint": "^8.72.1",
70
+ "vitest": "3.2.7"
71
+ },
72
+ "dsh": {
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ },
76
+ "client": {
77
+ "inject": [
78
+ "@deepseek-ai/dsh-client-locale"
79
+ ],
80
+ "platform": "web"
81
+ },
82
+ "compatibility": {
83
+ "dshReleases": {
84
+ "0.2.0-rc.2": "compatible",
85
+ "0.2.1-rc.1": "compatible",
86
+ "0.2.1-rc.2": "compatible",
87
+ "0.2.1-rc.3": "compatible",
88
+ "0.2.1-rc.5": "compatible"
89
+ }
90
+ }
91
+ },
92
+ "publishConfig": {
93
+ "access": "public"
94
+ },
95
+ "repository": {
96
+ "type": "git",
97
+ "url": "git+https://github.com/xrn1997/dsh-session-delete.git"
98
+ },
99
+ "homepage": "https://github.com/xrn1997/dsh-session-delete#readme",
100
+ "bugs": {
101
+ "url": "https://github.com/xrn1997/dsh-session-delete/issues"
102
+ },
103
+ "keywords": [
104
+ "dsh",
105
+ "deepseek-harness",
106
+ "plugin",
107
+ "session",
108
+ "trash",
109
+ "delete"
110
+ ],
111
+ "author": "xrn1997",
112
+ "license": "Apache-2.0",
113
+ "dependencies": {
114
+ "react-markdown": "^10.1.0",
115
+ "remark-gfm": "^4.0.1"
116
+ }
117
+ }
@@ -1,45 +0,0 @@
1
- /**
2
- * 请求栅栏:判一条打到 `/dsh-session-delete-api` 的请求归不归本插件办。
3
- *
4
- * 两件事分开:**归属**(Host / Origin / Referer 是不是本次部署自己的)与**身份**(远程客户端是不是
5
- * 宿主的已认证 Web 客户端)。绑定策略归宿主 webserver 配置——它只接受具体接口 IP 字面量、拒绝
6
- * wildcard——所以「谁能到达」不是本插件能替它决定的事;栅栏本身只防 browser confused deputy,
7
- * 身份留给宿主(见 ②)。口径与宿主 `/api` 的 `isTrustedApiRequest`(asar
8
- * `dsh-client-connection/lib/index.js`)对齐,差别只有一条:本模块多查 Referer,因为
9
- * `GET /attachment` 是二进制段(见末段)。
10
- *
11
- * 两条放行支,外加一条缺席规则:
12
- * ① 头栅栏全过 ∧ 源地址回环 ∧ Host 属于本次部署(回环名 / 监听 bind 字面量)——本机工具与转发落在
13
- * 回环的桌面壳;
14
- * ② 宿主 `connection` 服务的 `requestRejection` 放行——远程浏览器。它自带宿主的 Host 白名单
15
- * (含部署声明的 `trustedHosts`)与「已认证 live peer」,身份就是那次 `dsh web` 打印的 URL 换来的
16
- * 签名 cookie,不在本插件里复刻;
17
- * ③ 身份层缺席(`src/ports/connection-port.ts` 报 `absent`,宿主无 `connection` 服务或成员改名)
18
- * ⇒ 远程一律拒(fail-closed:退回「只有本机」,LAN 上表现为 403 而不是静默放行)。
19
- *
20
- * **DNS rebinding 由 Host 归属判据挡**:`Origin === Host` 只证两者同名,不证这个名字属于本部署;
21
- * 回环 socket 也不构成豁免(重绑到 `127.0.0.1` 的攻击正是回环源地址)。对浏览器来说 Host 是重绑伪造
22
- * 不了的那个头——它按连接目标填,而 `Origin` 是页面自己的名字——所以归属判定先于任何本机 / 远程
23
- * 分支,别在它前面加 fast-path。
24
- *
25
- * 末段:`GET /attachment` 的响应不是 opaque 的「读不出内容」——页面若已知某个 `sha256:…` 内容哈希,
26
- * 就能把它当 `<img src>` 跨源渲染出来。那条请求不带 Origin,靠 Referer 判同源挡;宿主 `/api` 没有
27
- * 二进制段,所以它的栅栏不需要这一条。
28
- */
29
- import type { IncomingMessage } from 'node:http';
30
- import type { ConnectionPort } from '../ports/connection-port.js';
31
- export type RequestTrustVerdict = 'allowed' | 'untrusted' | 'unauthenticated';
32
- export interface RequestTrustFacts {
33
- /** 监听器绑定的那个具体接口 IP 字面量;老代次没有这个 getter 时为 `undefined`(bind 支退化,仍 fail-closed)。 */
34
- readonly bindHost: string | undefined;
35
- /** 宿主的 Web 身份面;端口每次请求惰性解析服务,缺席由它报 `absent`。 */
36
- readonly connection: ConnectionPort;
37
- }
38
- /**
39
- * 这条请求归不归本插件办。`untrusted` = 归属不对(跨源 / 重绑 / 头不成形);`unauthenticated` =
40
- * 归属对但远程客户端没带宿主身份(原文会提示用 `dsh web` 打印的 URL 打开)。
41
- *
42
- * 远程支的两个失败裁决由端口翻译宿主的区分(401 = 缺身份、403 = 归属不对)。身份层缺席(端口报
43
- * `absent`)一律按归属不对处理:那时没有可换的身份,不能让原文提示用户去补一个不存在的东西。
44
- */
45
- export declare function judgeRequest(req: IncomingMessage, facts: RequestTrustFacts): RequestTrustVerdict;
@@ -1,33 +0,0 @@
1
- /**
2
- * 宿主 `connection` 服务(`dsh-client-connection` 的 `HostConnectionService`,`super(ctx, "connection")`)
3
- * 的窄镜像:`requestRejection(request)` 一次做宿主 `/api` 的那两道——Host/Origin 栅栏与「已认证 live peer」。
4
- *
5
- * **每次请求惰性解析服务**,不在激活时取一次:插件与 `client-connection` 的激活顺序没有承诺,取早了
6
- * 会让远程支永远缺席(LAN 上全 403 且难排查)。`ctx.get` 是同步查表,代价可以忽略。
7
- *
8
- * **状态码在这里翻成裁决**(401 = 归属对、缺身份;403 = 归属不对;其余按归属不对处理),
9
- * 调用方拿到的就是自己的词表,不需要知道宿主用哪个码表达哪件事。成员名与返回值按 asar
10
- * `dsh-client-connection/lib/index.js` 原文读实。
11
- */
12
- import type { IncomingMessage } from 'node:http';
13
- import type { Context } from '@deepseek-ai/cordis';
14
- export type ConnectionVerdict = {
15
- readonly kind: 'allowed';
16
- }
17
- /** 组成里没有这个服务(无 web 组成 / 老代次)——调用方 fail-closed。 */
18
- | {
19
- readonly kind: 'absent';
20
- }
21
- /** 宿主认可归属,但这条请求没带已认证身份。 */
22
- | {
23
- readonly kind: 'unauthenticated';
24
- }
25
- /** 宿主不认可归属(Host/Origin 栅栏不过,或它给出的其余状态码)。 */
26
- | {
27
- readonly kind: 'untrusted';
28
- };
29
- export interface ConnectionPort {
30
- verdictFor(req: IncomingMessage): ConnectionVerdict;
31
- }
32
- /** 成员抛错按「归属不对」拒绝(fail-closed)。 */
33
- export declare function createConnectionPort(ctx: Context): ConnectionPort;