dsh-media-converter 1.2.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/README.md +10 -2
  2. package/dist/index.js +36 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,6 +19,12 @@ DeepSeek Harness 的本地多媒体工具箱插件:转换、裁剪、提取音
19
19
 
20
20
  界面是一个「动作面板」:选文件 → 搜索或点动作 → 参数就地展开 → 结果就地显示。支持 `⌘K` 聚焦搜索、回车执行第一个动作、Esc 收起。
21
21
 
22
+ ![媒体工具箱空状态:拖入文件或点击选择,下方显示输出目录](docs/screenshots/panel-empty.png)
23
+
24
+ 选中一张图片后:左侧原图、右侧实时预览,并排 / 滑块两种对比方式,下方是「转换 / 查看」分组的动作列表。
25
+
26
+ ![图片前后对比与动作列表](docs/screenshots/image-compare.png)
27
+
22
28
  ## 给 Agent 用的工具
23
29
 
24
30
  同一个引擎也注册成工具 `media_toolbox`,助手可以直接调用:
@@ -44,12 +50,14 @@ DeepSeek Harness 的本地多媒体工具箱插件:转换、裁剪、提取音
44
50
 
45
51
  ## 安装
46
52
 
47
- 已发布到 npm(`dsh-media-converter`)。在 Harness 里用 `plugin_manager` 的 `install_bundle` 装,或在 profile 目录里:
53
+ 已发布到 npm(`dsh-media-converter`)。一行装完:
48
54
 
49
55
  ```sh
50
- dsh plugin add dsh-media-converter --profile <profile>
56
+ dsh plugin --profile web add dsh-media-converter
51
57
  ```
52
58
 
59
+ `web` 换成你自己的 profile 名即可。也可以在 Harness 里用 `plugin_manager` 的 `install_bundle` 装。
60
+
53
61
  从源码装(开发用):`plugin_manager` 的 `install_bundle`,target 指向本仓库目录——它会在 profile 的 `package.json` 里登记包名,并在 `node_modules/` 下建一条软链指向本目录。
54
62
 
55
63
  从 GitHub 装(未发布 npm 时的退路):
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  // DSH Host plugin: local media toolkit — convert, trim, extract tracks, and
2
2
  // move media to/from Base64 through ffmpeg.
3
3
  //
4
+ // Security has one home: every route asks the composition's `connection`
5
+ // service for a rejection first (the Host/Origin fence plus browser
6
+ // authentication, the same guard the `/api` bridge applies). No connection
7
+ // service means no trusted caller, so requests are refused rather than served.
8
+ //
4
9
  // The Web page (client.js) never touches the filesystem: it uploads picked
5
- // files to this plugin's same-origin routes, the Host runs one ffmpeg process
10
+ // files to this plugin's routes, the Host runs one ffmpeg process
6
11
  // per job, and the page polls the job for progress. Results land in
7
12
  // ~/Downloads/media-toolbox (config.outputDir overrides it) and are served back
8
13
  // for preview from that same directory only.
@@ -241,18 +246,17 @@ export function apply(ctx, config) {
241
246
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
242
247
  res.end(JSON.stringify(payload));
243
248
  };
244
- // Mutating routes are same-origin only: a plain cross-origin POST is a
245
- // "simple request" the browser sends without preflight.
246
- const sameOrigin = (req) => {
247
- const origin = req.headers.origin;
248
- if (!origin)
249
- return true;
250
- try {
251
- return new URL(origin).host === new URL(webServer.getOrigin()).host;
252
- }
253
- catch {
249
+ // Every route crosses the connection trust fence before it touches the body
250
+ // or the disk: 403 answers a foreign Host/Origin, 401 a caller without the
251
+ // browser-session cookie. 503 when the composition owns no connection
252
+ // service — fail closed, never serve an unauthenticated request.
253
+ const connection = ctx.get('connection');
254
+ const rejectUnauthenticated = (req, res) => {
255
+ const rejection = connection ? connection.requestRejection({ headers: req.headers }) : 503;
256
+ if (rejection === undefined)
254
257
  return false;
255
- }
258
+ json(res, rejection, errorPayload('unauthorized'));
259
+ return true;
256
260
  };
257
261
  // The preview and the reveal may only reach into the output directory. The
258
262
  // check resolves both sides first: a plain prefix check is defeated by a
@@ -605,6 +609,8 @@ export function apply(ctx, config) {
605
609
  handler: (req, res) => {
606
610
  if (req.method !== 'GET')
607
611
  return json(res, 405, errorPayload('GET only'));
612
+ if (rejectUnauthenticated(req, res))
613
+ return;
608
614
  json(res, 200, statusPayload(ffmpeg, outDir, Math.round(maxUploadBytes / 1048576), version, composerButton, groups));
609
615
  },
610
616
  }),
@@ -614,8 +620,8 @@ export function apply(ctx, config) {
614
620
  handler: async (req, res) => {
615
621
  if (req.method !== 'POST')
616
622
  return json(res, 405, errorPayload('POST only'));
617
- if (!sameOrigin(req))
618
- return json(res, 403, errorPayload('forbidden origin'));
623
+ if (rejectUnauthenticated(req, res))
624
+ return;
619
625
  try {
620
626
  const body = await readJson(req);
621
627
  // Only defined keys are merged; an empty string resets that key to
@@ -670,8 +676,8 @@ export function apply(ctx, config) {
670
676
  handler: async (req, res) => {
671
677
  if (req.method !== 'POST')
672
678
  return json(res, 405, errorPayload('POST only'));
673
- if (!sameOrigin(req))
674
- return json(res, 403, errorPayload('forbidden origin'));
679
+ if (rejectUnauthenticated(req, res))
680
+ return;
675
681
  if (!ffmpeg)
676
682
  return json(res, 200, noFfmpeg);
677
683
  let file = '';
@@ -740,8 +746,8 @@ export function apply(ctx, config) {
740
746
  handler: async (req, res) => {
741
747
  if (req.method !== 'POST')
742
748
  return json(res, 405, errorPayload('POST only'));
743
- if (!sameOrigin(req))
744
- return json(res, 403, errorPayload('forbidden origin'));
749
+ if (rejectUnauthenticated(req, res))
750
+ return;
745
751
  if (!ffmpeg)
746
752
  return json(res, 200, noFfmpeg);
747
753
  try {
@@ -763,8 +769,8 @@ export function apply(ctx, config) {
763
769
  handler: async (req, res) => {
764
770
  if (req.method !== 'POST')
765
771
  return json(res, 405, errorPayload('POST only'));
766
- if (!sameOrigin(req))
767
- return json(res, 403, errorPayload('forbidden origin'));
772
+ if (rejectUnauthenticated(req, res))
773
+ return;
768
774
  try {
769
775
  const body = await readJson(req);
770
776
  const job = jobs.get(String(body.id || ''));
@@ -800,6 +806,8 @@ export function apply(ctx, config) {
800
806
  handler: (req, res) => {
801
807
  if (req.method !== 'GET')
802
808
  return json(res, 405, errorPayload('GET only'));
809
+ if (rejectUnauthenticated(req, res))
810
+ return;
803
811
  const id = requestUrl(req).searchParams.get('id');
804
812
  const job = id ? jobs.get(id) : undefined;
805
813
  if (!job)
@@ -813,6 +821,8 @@ export function apply(ctx, config) {
813
821
  handler: async (req, res) => {
814
822
  if (req.method !== 'GET')
815
823
  return json(res, 405, errorPayload('GET only'));
824
+ if (rejectUnauthenticated(req, res))
825
+ return;
816
826
  const file = requestUrl(req).searchParams.get('file');
817
827
  if (!insideOutput(file))
818
828
  return json(res, 403, errorPayload('只能预览输出目录里的文件'));
@@ -825,8 +835,8 @@ export function apply(ctx, config) {
825
835
  handler: async (req, res) => {
826
836
  if (req.method !== 'POST')
827
837
  return json(res, 405, errorPayload('POST only'));
828
- if (!sameOrigin(req))
829
- return json(res, 403, errorPayload('forbidden origin'));
838
+ if (rejectUnauthenticated(req, res))
839
+ return;
830
840
  try {
831
841
  const { file } = await readJson(req);
832
842
  if (!insideOutput(file))
@@ -846,8 +856,8 @@ export function apply(ctx, config) {
846
856
  handler: async (req, res) => {
847
857
  if (req.method !== 'POST')
848
858
  return json(res, 405, errorPayload('POST only'));
849
- if (!sameOrigin(req))
850
- return json(res, 403, errorPayload('forbidden origin'));
859
+ if (rejectUnauthenticated(req, res))
860
+ return;
851
861
  let upload;
852
862
  let mime;
853
863
  try {
@@ -887,8 +897,8 @@ export function apply(ctx, config) {
887
897
  handler: async (req, res) => {
888
898
  if (req.method !== 'POST')
889
899
  return json(res, 405, errorPayload('POST only'));
890
- if (!sameOrigin(req))
891
- return json(res, 403, errorPayload('forbidden origin'));
900
+ if (rejectUnauthenticated(req, res))
901
+ return;
892
902
  try {
893
903
  const body = await readJson(req);
894
904
  const payload = decodeBase64Payload(body.text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-media-converter",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Local media toolbox for DeepSeek Harness: convert, trim, split tracks, export subtitles, compress images, edit metadata, and Base64 — all through local ffmpeg.",
5
5
  "keywords": [
6
6
  "dsh",