deepseek-harness-background 0.2.0 → 0.3.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.
package/lib/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes } from "node:crypto";
4
- import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
- import { extname, join } from "node:path";
4
+ import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { readFile } from "node:fs/promises";
6
+ import { extname, join, resolve } from "node:path";
6
7
  import { homedir } from "node:os";
7
8
  //#region src/settings.ts
8
9
  /** Durable background settings — pure constants/types shared by both halves. */
@@ -23,14 +24,17 @@ const BACKGROUND_SETTINGS_FIELDS = [
23
24
  "panelOpacity",
24
25
  "blur",
25
26
  "wallpaperBlur",
26
- "fit"
27
+ "fit",
28
+ "timeline"
27
29
  ];
28
30
  /** Browser-facing base path of the background API (shared with the host routes). */
29
31
  const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
30
32
  /** Minimum scrim opacity (0 = no veil at all). */
31
33
  const SCRIM_MIN = 0;
32
- /** Maximum scrim opacity (a nearly opaque veil keeps text legible over any art). */
33
- const SCRIM_MAX = .95;
34
+ /** Maximum scrim opacity (1 = fully opaque veil — hides the wallpaper entirely).
35
+ * Kept at 1 so every percent-unit slider shares one 0..1 domain: the same
36
+ * displayed percentage puts every thumb at the same fraction of its track. */
37
+ const SCRIM_MAX = 1;
34
38
  /** Default scrim strength applied over user art (a light readability veil). */
35
39
  const DEFAULT_SCRIM = .25;
36
40
  /** Default background image opacity (1 = fully opaque). */
@@ -61,35 +65,46 @@ const BackgroundSettingsSchema = z.object({
61
65
  /** Image URL or empty (mutually exclusive with uploadId at runtime). */
62
66
  url: z.string().default(""),
63
67
  opacity: z.number().min(0).max(1).default(1),
64
- scrim: z.number().min(0).max(SCRIM_MAX).default(DEFAULT_SCRIM),
68
+ scrim: z.number().min(0).max(1).default(DEFAULT_SCRIM),
65
69
  panelOpacity: z.number().min(0).max(1).default(DEFAULT_PANEL_OPACITY),
66
70
  blur: z.number().min(0).max(40).default(16),
67
71
  wallpaperBlur: z.number().min(0).max(60).default(0),
68
- fit: z.union([...FIT_MODES]).default(DEFAULT_FIT)
72
+ fit: z.union([...FIT_MODES]).default(DEFAULT_FIT),
73
+ timeline: z.boolean().default(true)
69
74
  });
70
75
  //#endregion
71
76
  //#region src/harness-home.ts
72
77
  /**
73
78
  * Harness-home resolution — where the uploaded wallpaper images live.
74
79
  *
75
- * Mirrors the dsh launcher's precedence (see apps/cli reference and the
76
- * skin-center harness-home.ts in dsh-web-ui): an explicit `--home`-formatted
77
- * override, else `$DSH_HOME`, else `<homedir>/.dsh`. Kept as a tiny pure
80
+ * Mirrors the dsh launcher's precedence (see @deepseek-ai/dsh-home-paths'
81
+ * resolveDshHome): `$DSH_HOME`, else `<homedir>/.dsh`. Kept as a tiny pure
78
82
  * function so the host half needs no out-of-tree dependency on the internal
79
- * home-paths package.
83
+ * home-paths package. The semantics match the official resolver: an empty or
84
+ * whitespace-only `$DSH_HOME` counts as unset, a leading `~` / `~/` / `~\`
85
+ * expands against the OS home, and the result is normalized with
86
+ * path.resolve().
80
87
  */
81
88
  /** First non-blank string in a candidate list, else undefined. */
82
89
  function firstNonBlank(...values) {
83
90
  for (const value of values) if (typeof value === "string" && value.trim() !== "") return value.trim();
84
91
  }
92
+ /** Expand supported tilde prefixes against the OS home (mirrors expandHomePath). */
93
+ function expandHomePath(path) {
94
+ if (path === "~") return homedir();
95
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
96
+ return path;
97
+ }
85
98
  /**
86
- * Resolve the harness home (the directory containing `profiles/`): explicit
87
- * override wins, then `$DSH_HOME`, then `~/.dsh`.
99
+ * Resolve the harness home (the directory containing `profiles/`): `$DSH_HOME`
100
+ * first, then `~/.dsh`. An empty/whitespace `$DSH_HOME` counts as unset; a
101
+ * leading tilde expands against the OS home; the result is absolute.
88
102
  * @param env - environment (defaults to process.env).
89
- * @returns the absolute harness home directory.
103
+ * @returns the normalized absolute harness home directory.
90
104
  */
91
105
  function resolveHarnessHome(env = process.env) {
92
- return firstNonBlank(env.DSH_HOME) ?? join(homedir(), ".dsh");
106
+ const selected = firstNonBlank(env.DSH_HOME) ?? join(homedir(), ".dsh");
107
+ return resolve(expandHomePath(selected));
93
108
  }
94
109
  /** Subdirectory under the harness home that this plugin owns. */
95
110
  const PLUGIN_HOME_REL = join("deepseek-harness-background");
@@ -122,7 +137,8 @@ const DEFAULTS = {
122
137
  panelOpacity: DEFAULT_PANEL_OPACITY,
123
138
  blur: 16,
124
139
  wallpaperBlur: 0,
125
- fit: DEFAULT_FIT
140
+ fit: DEFAULT_FIT,
141
+ timeline: true
126
142
  };
127
143
  /** Keep only the schema's known fields (see BACKGROUND_SETTINGS_FIELDS). */
128
144
  function pickKnown(section) {
@@ -149,7 +165,7 @@ const NUM_BOUNDS = {
149
165
  },
150
166
  scrim: {
151
167
  min: 0,
152
- max: SCRIM_MAX
168
+ max: 1
153
169
  },
154
170
  panelOpacity: {
155
171
  min: 0,
@@ -198,19 +214,42 @@ function requireMethod(req, res, method) {
198
214
  * the request `Host` is rejected. Requests without either header (curl, node
199
215
  * http) pass — this is a local single-user tool, and the fence only targets
200
216
  * the cross-site browser vector.
217
+ *
218
+ * The `Host` itself must also be a loopback name: under a DNS-rebinding
219
+ * attack the attacker's page resolves their own domain to 127.0.0.1, so both
220
+ * `Origin` and `Host` carry the attacker's hostname and the self-consistency
221
+ * check above alone would admit them. The web server binds loopback (or is
222
+ * deliberately exposed on 0.0.0.0 by its operator), so accepting only
223
+ * 127.0.0.1 / localhost / [::1] keeps every legitimate same-origin client
224
+ * working while refusing rebounded ones.
201
225
  */
226
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
227
+ "127.0.0.1",
228
+ "localhost",
229
+ "[::1]",
230
+ "::1"
231
+ ]);
232
+ function isLoopbackHost(host) {
233
+ let bare = host;
234
+ if (host.startsWith("[")) {
235
+ const end = host.indexOf("]");
236
+ if (end !== -1) bare = host.slice(0, end + 1);
237
+ } else {
238
+ const colon = host.indexOf(":");
239
+ if (colon !== -1) bare = host.slice(0, colon);
240
+ }
241
+ return LOOPBACK_HOSTS.has(bare) || LOOPBACK_HOSTS.has(host);
242
+ }
202
243
  function isSameOriginRequest(req) {
203
244
  const site = req.headers["sec-fetch-site"];
204
245
  if (typeof site === "string" && site === "cross-site") return false;
246
+ const host = req.headers.host;
247
+ if (typeof host !== "string" || !isLoopbackHost(host)) return false;
205
248
  const origin = req.headers.origin;
206
- if (typeof origin === "string" && origin !== "" && origin !== "null") {
207
- const host = req.headers.host;
208
- if (typeof host !== "string" || host === "") return false;
209
- try {
210
- return new URL(origin).host === host;
211
- } catch {
212
- return false;
213
- }
249
+ if (typeof origin === "string" && origin !== "" && origin !== "null") try {
250
+ return new URL(origin).host === host;
251
+ } catch {
252
+ return false;
214
253
  }
215
254
  return true;
216
255
  }
@@ -292,6 +331,12 @@ function validateSectionBody(body, home) {
292
331
  if (raw === void 0) continue;
293
332
  if (typeof raw !== "number" || !Number.isFinite(raw) || raw < min || raw > max) return "invalid-range";
294
333
  }
334
+ const enabled = body.enabled;
335
+ if (enabled !== void 0 && typeof enabled !== "boolean") return "invalid-enabled";
336
+ const timeline = body.timeline;
337
+ if (timeline !== void 0 && typeof timeline !== "boolean") return "invalid-timeline";
338
+ const fit = body.fit;
339
+ if (fit !== void 0 && fit !== "" && !FIT_MODES.includes(fit)) return "invalid-fit";
295
340
  return null;
296
341
  }
297
342
  /**
@@ -324,13 +369,14 @@ function storeUpload(bytes, mime, homeOverride) {
324
369
  url: `${BACKGROUND_API_PREFIX}/image/${id}`
325
370
  };
326
371
  }
327
- /** Resolve an upload id to its on-disk path (or "" if invalid / missing). */
372
+ /** Resolve an upload id to its on-disk path (or "" if invalid / missing).
373
+ * Read-only: unlike the write path this never creates the plugin home, so a
374
+ * GET cannot pay for (or race) a mkdirSync on every request. */
328
375
  function uploadPath(id, homeOverride) {
329
376
  if (!isUploadId(id)) return "";
330
- const dir = ensurePluginHome(homeOverride);
377
+ const dir = pluginHome(homeOverride);
331
378
  for (const ext of [
332
379
  "jpg",
333
- "jpeg",
334
380
  "png",
335
381
  "webp",
336
382
  "gif"
@@ -478,7 +524,7 @@ function makeBackgroundRoutes(settings, opts = {}) {
478
524
  {
479
525
  kind: "prefix",
480
526
  path: `${BACKGROUND_API_PREFIX}/image`,
481
- handler: (req, res) => {
527
+ handler: async (req, res) => {
482
528
  if (!isSameOriginRequest(req)) {
483
529
  json(res, 403, {
484
530
  ok: false,
@@ -497,11 +543,12 @@ function makeBackgroundRoutes(settings, opts = {}) {
497
543
  return;
498
544
  }
499
545
  try {
500
- const data = readFileSync(abs);
546
+ const data = await readFile(abs);
501
547
  const mime = mimeForPath(abs) ?? "application/octet-stream";
502
548
  const stat = statSync(abs);
503
549
  res.writeHead(200, {
504
550
  "content-type": mime,
551
+ "x-content-type-options": "nosniff",
505
552
  "content-length": String(data.length),
506
553
  "last-modified": stat.mtime.toUTCString(),
507
554
  "cache-control": "public, max-age=31536000, immutable"
@@ -528,7 +575,7 @@ const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
528
575
  */
529
576
  function apply(ctx) {
530
577
  ctx.inject(["settings", "webServer"], (hostCtx) => {
531
- hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
578
+ if (!hostCtx.settings.describe().some((descriptor) => descriptor.ns === "ui-background")) hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
532
579
  try {
533
580
  for (const route of makeBackgroundRoutes(hostCtx.settings)) hostCtx.effect(() => hostCtx.webServer.register(route), "deepseek-harness-background: settings route");
534
581
  } catch (error) {
package/package.json CHANGED
@@ -1,88 +1,86 @@
1
- {
2
- "name": "deepseek-harness-background",
3
- "description": "Custom background image plugin for the DeepSeek Harness web GUI: upload a local picture or paste an image URL, and render it behind the whole app surface with adjustable opacity, scrim, panel transparency and frosted-glass blur.",
4
- "version": "0.2.0",
5
- "keywords": [
6
- "deepseek-harness",
7
- "dsh",
8
- "plugin",
9
- "dsh-plugin",
10
- "theme",
11
- "wallpaper",
12
- "background",
13
- "web-ui"
14
- ],
15
- "author": "HaoyueQin",
16
- "license": "MIT",
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/HaoyueQin/deepseek-harness-background.git"
20
- },
21
- "homepage": "https://github.com/HaoyueQin/deepseek-harness-background",
22
- "packageManager": "pnpm@11.9.0",
23
- "type": "module",
24
- "main": "lib/index.js",
25
- "exports": {
26
- ".": "./lib/index.js",
27
- "./client": "./lib/client.js",
28
- "./src/*": "./src/*",
29
- "./package.json": "./package.json"
30
- },
31
- "dsh": {
32
- "bundle": {
33
- "patch": "./cordis.patch.yml"
34
- },
35
- "client": {
36
- "inject": [
37
- "@deepseek-ai/dsh-client-runtime",
38
- "@deepseek-ai/dsh-client-locale"
39
- ],
40
- "platform": "web"
41
- }
42
- },
43
- "scripts": {
44
- "build": "tsdown",
45
- "prepare": "tsdown",
46
- "watch": "tsdown --watch",
47
- "test": "vitest run",
48
- "typecheck": "tsc --noEmit -p tsconfig.json"
49
- },
50
- "license": "MIT",
51
- "files": [
52
- "lib",
53
- "cordis.patch.yml",
54
- "README.md",
55
- "README.zh.md"
56
- ],
57
- "peerDependencies": {
58
- "@deepseek-ai/cordis": "^4.0.1",
59
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
60
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
61
- "react": "^18.2.0"
62
- },
63
- "dependencies": {
64
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
65
- "@deepseek-ai/schemastery": "^3.18.1"
66
- },
67
- "devDependencies": {
68
- "@deepseek-ai/cordis": "^4.0.1",
69
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
70
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
71
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
72
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
73
- "@deepseek-ai/schemastery": "^3.18.1",
74
- "@testing-library/dom": "^10.4.1",
75
- "@testing-library/react": "^16.3.0",
76
- "@types/node": "^22.0.0",
77
- "@types/react": "~18.3.1",
78
- "@types/react-dom": "~18.3.0",
79
- "clsx": "^2.1.1",
80
- "jsdom": "^29.1.1",
81
- "lightningcss": "^1.33.0",
82
- "react": "^18.2.0",
83
- "react-dom": "^18.2.0",
84
- "tsdown": "^0.22.14",
85
- "typescript": "^5.8.0",
86
- "vitest": "^4.1.8"
87
- }
88
- }
1
+ {
2
+ "name": "deepseek-harness-background",
3
+ "description": "Custom background image plugin for the DeepSeek Harness web GUI: upload a local picture or paste an image URL, and render it behind the whole app surface with adjustable opacity, scrim, panel transparency and frosted-glass blur.",
4
+ "version": "0.3.1",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "plugin",
9
+ "dsh-plugin",
10
+ "theme",
11
+ "wallpaper",
12
+ "background",
13
+ "web-ui"
14
+ ],
15
+ "author": "HaoyueQin",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/HaoyueQin/deepseek-harness-background.git"
20
+ },
21
+ "homepage": "https://github.com/HaoyueQin/deepseek-harness-background",
22
+ "packageManager": "pnpm@11.9.0",
23
+ "type": "module",
24
+ "main": "lib/index.js",
25
+ "exports": {
26
+ ".": "./lib/index.js",
27
+ "./client": "./lib/client.js",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "inject": [
36
+ "@deepseek-ai/dsh-client-runtime",
37
+ "@deepseek-ai/dsh-client-locale"
38
+ ],
39
+ "platform": "web"
40
+ }
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "prepare": "tsdown",
45
+ "watch": "tsdown --watch",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc --noEmit -p tsconfig.json"
48
+ },
49
+ "files": [
50
+ "lib",
51
+ "cordis.patch.yml",
52
+ "README.md",
53
+ "README.zh.md"
54
+ ],
55
+ "peerDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.1",
57
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
58
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
59
+ "react": "^18.2.0"
60
+ },
61
+ "dependencies": {
62
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
63
+ "@deepseek-ai/schemastery": "^3.18.1"
64
+ },
65
+ "devDependencies": {
66
+ "@deepseek-ai/cordis": "^4.0.1",
67
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
69
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
70
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
71
+ "@deepseek-ai/schemastery": "^3.18.1",
72
+ "@testing-library/dom": "^10.4.1",
73
+ "@testing-library/react": "^16.3.0",
74
+ "@types/node": "^22.0.0",
75
+ "@types/react": "~18.3.1",
76
+ "@types/react-dom": "~18.3.0",
77
+ "clsx": "^2.1.1",
78
+ "jsdom": "^29.1.1",
79
+ "lightningcss": "^1.33.0",
80
+ "react": "^18.2.0",
81
+ "react-dom": "^18.2.0",
82
+ "tsdown": "^0.22.14",
83
+ "typescript": "^5.8.0",
84
+ "vitest": "^4.1.8"
85
+ }
86
+ }