@linxin666/dsh-client-ui-skin-center 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/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import { chmodSync, closeSync, cpSync, createReadStream, existsSync, lstatSync,
4
4
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { homedir } from "node:os";
7
+ import { createHash } from "node:crypto";
7
8
  import { transform } from "lightningcss";
8
9
  import { execFile, execFileSync } from "node:child_process";
9
10
  import { readFile, stat } from "node:fs/promises";
@@ -703,6 +704,65 @@ function resolveHarnessPaths(home, profile, fromUrl = import.meta.url) {
703
704
  };
704
705
  }
705
706
  //#endregion
707
+ //#region src/provenance.ts
708
+ /**
709
+ * Official-market provenance verification (issue #1073).
710
+ *
711
+ * Skins installed one-click from the DSH Market carry a
712
+ * dsh-market.provenance.json written by the market installer at install
713
+ * time, pinning every installed file to its sha256 and to the market
714
+ * origin. The market's skin content is built from THIS repository (same
715
+ * review, same release), so when the on-disk skin.json and hooks entry
716
+ * hash-match the provenance, the hooks bytes are exactly the reviewed
717
+ * bytes and may run like a built-in skin's.
718
+ *
719
+ * Fail-closed: a missing/unparseable provenance, a foreign source, or any
720
+ * hash mismatch (post-install tampering, partial copy) keeps the
721
+ * hooks-refused behavior for user-directory skins. Forging the provenance
722
+ * requires write access to $DSH_HOME itself — an attacker with that access
723
+ * can already install full plugins, so the file is a provenance record,
724
+ * not a capability guard against the local user.
725
+ * @module @linxin666/dsh-client-ui-skin-center/provenance
726
+ */
727
+ /** Provenance filename written by the market installer (mirrors PROVENANCE_FILENAME in @linxin666/dsh-client-ui-market; no cross-package runtime import). */
728
+ const MARKET_PROVENANCE_FILENAME = "dsh-market.provenance.json";
729
+ function sha256Hex(abs) {
730
+ try {
731
+ return createHash("sha256").update(readFileSync(abs)).digest("hex");
732
+ } catch {
733
+ return null;
734
+ }
735
+ }
736
+ /**
737
+ * Whether the skin directory at dir carries valid official-market
738
+ * provenance for skinId whose declared hooks entry (already validated as a
739
+ * safe relative path by the manifest validator) hash-matches the recorded
740
+ * bytes — skin.json included, so the facet entry path itself is pinned.
741
+ */
742
+ function verifyMarketProvenance(dir, skinId, hooksEntry) {
743
+ let raw;
744
+ try {
745
+ raw = JSON.parse(readFileSync(join(dir, MARKET_PROVENANCE_FILENAME), "utf8"));
746
+ } catch {
747
+ return false;
748
+ }
749
+ if (typeof raw !== "object" || raw === null) return false;
750
+ const prov = raw;
751
+ if (prov.version !== 1) return false;
752
+ if (prov.source !== "https://dsh-market.com") return false;
753
+ if (prov.id !== skinId) return false;
754
+ const files = prov.files;
755
+ if (typeof files !== "object" || files === null) return false;
756
+ const hashes = files;
757
+ for (const rel of ["skin.json", hooksEntry]) {
758
+ const expected = hashes[rel];
759
+ if (typeof expected !== "string" || !/^[0-9a-f]{64}$/.test(expected)) return false;
760
+ const actual = sha256Hex(join(dir, ...rel.split("/")));
761
+ if (actual === null || actual !== expected) return false;
762
+ }
763
+ return true;
764
+ }
765
+ //#endregion
706
766
  //#region src/skin-repo.ts
707
767
  /**
708
768
  * Skin repository (issue #506, M2): dual-source discovery of v2 skin asset
@@ -789,6 +849,28 @@ function readManifest(dir) {
789
849
  return null;
790
850
  }
791
851
  }
852
+ /**
853
+ * Hooks trust for one user-directory skin: official-market installs
854
+ * whose skin.json and hooks entry hash-match the recorded provenance
855
+ * run their hooks (same-review content); anything else keeps the
856
+ * refusal warning. Built-in skins never reach this — their origin
857
+ * is the trust signal.
858
+ */
859
+ function marketHooksTrust(manifest, dir) {
860
+ const facet = manifest.facets?.client;
861
+ if (!facet) return {
862
+ trusted: false,
863
+ warning: null
864
+ };
865
+ if (verifyMarketProvenance(dir, manifest.id, facet.entry)) return {
866
+ trusted: true,
867
+ warning: null
868
+ };
869
+ return {
870
+ trusted: false,
871
+ warning: "declares hooks.mjs, but hooks only run for built-in or verified official-market (same-review) skins; the hooks facet will be refused"
872
+ };
873
+ }
792
874
  function collectSource(spec, catalog, claimed) {
793
875
  if (!existsSync(spec.root)) return;
794
876
  let dirNames;
@@ -831,12 +913,14 @@ function collectSource(spec, catalog, claimed) {
831
913
  if (spec.origin === "user" && existing.origin === "builtin") {
832
914
  catalog.skins = catalog.skins.filter((s) => s !== existing);
833
915
  const winnerWarnings = [...result.warnings, `shadows the built-in "${manifest.id}" skin`];
834
- if (manifest.facets?.client) winnerWarnings.push("declares hooks.mjs, but hooks only run for built-in (same-review) skins; the hooks facet will be refused");
916
+ const trust = marketHooksTrust(manifest, dir);
917
+ if (trust.warning !== null) winnerWarnings.push(trust.warning);
835
918
  const winner = {
836
919
  manifest,
837
920
  origin: "user",
838
921
  dir,
839
- warnings: winnerWarnings
922
+ warnings: winnerWarnings,
923
+ ...trust.trusted ? { hooksTrusted: true } : {}
840
924
  };
841
925
  claimed.set(manifest.id, winner);
842
926
  catalog.skins.push(winner);
@@ -844,14 +928,19 @@ function collectSource(spec, catalog, claimed) {
844
928
  continue;
845
929
  }
846
930
  const warnings = [...result.warnings];
847
- if (spec.origin === "user" && manifest.facets?.client) warnings.push("declares hooks.mjs, but hooks only run for built-in (same-review) skins; the hooks facet will be refused");
931
+ const trust = spec.origin === "user" ? marketHooksTrust(manifest, dir) : {
932
+ trusted: false,
933
+ warning: null
934
+ };
935
+ if (trust.warning !== null) warnings.push(trust.warning);
848
936
  const contractWarnings = auditTokenContract(stylesheetEntries(manifest, dir));
849
937
  warnings.push(...contractWarnings.warnings);
850
938
  const entry = {
851
939
  manifest,
852
940
  origin: spec.origin,
853
941
  dir,
854
- warnings
942
+ warnings,
943
+ ...trust.trusted ? { hooksTrusted: true } : {}
855
944
  };
856
945
  claimed.set(manifest.id, entry);
857
946
  catalog.skins.push(entry);
@@ -974,18 +1063,21 @@ const DEFAULT_SKIN_ID = "blue-fantasy";
974
1063
  function defaultActiveStatePath() {
975
1064
  return join(userSkinsDir(), "..", "skin-center-active.json");
976
1065
  }
977
- /** Read the whole state document; unreadable data yields all-null fields. */
1066
+ /** Read the whole state document; unreadable data yields all-null fields and initialized=false. */
978
1067
  function readActiveState(path) {
979
1068
  try {
980
1069
  const parsed = JSON.parse(readFileSync(path, "utf8"));
1070
+ const hasInitialized = typeof parsed.initialized === "boolean" ? parsed.initialized : typeof parsed === "object" && parsed !== null && ("active" in parsed || "background" in parsed);
981
1071
  return {
982
1072
  active: typeof parsed.active === "string" ? parsed.active : null,
983
- background: parsed.background === void 0 || parsed.background === null ? null : normalizeSkinBackground(parsed.background)
1073
+ background: parsed.background === void 0 || parsed.background === null ? null : normalizeSkinBackground(parsed.background),
1074
+ initialized: hasInitialized
984
1075
  };
985
1076
  } catch {
986
1077
  return {
987
1078
  active: null,
988
- background: null
1079
+ background: null,
1080
+ initialized: false
989
1081
  };
990
1082
  }
991
1083
  }
@@ -1007,7 +1099,10 @@ function writeActiveState(path, update) {
1007
1099
  mkdirSync(dir, { recursive: true });
1008
1100
  const tmpDir = mkdtempSync(join(dir, `${basename(path)}.tmp-`));
1009
1101
  const tmp = join(tmpDir, basename(path));
1010
- const document = { active };
1102
+ const document = {
1103
+ active,
1104
+ initialized: true
1105
+ };
1011
1106
  if (background !== null) document.background = background;
1012
1107
  try {
1013
1108
  writeFileSync(tmp, JSON.stringify(document, null, 2) + "\n", {
@@ -1037,7 +1132,8 @@ function writeActiveSelection(path, id) {
1037
1132
  * @returns whether the seed wrote the selection.
1038
1133
  */
1039
1134
  function seedDefaultActiveSkin(path, find) {
1040
- if (readActiveSelection(path) !== null) return false;
1135
+ const state = readActiveState(path);
1136
+ if (state.initialized || state.active !== null) return false;
1041
1137
  if (!find("blue-fantasy")) return false;
1042
1138
  writeActiveSelection(path, DEFAULT_SKIN_ID);
1043
1139
  return true;
@@ -1803,7 +1899,9 @@ function findCloseBrace(css, openBrace) {
1803
1899
  * The stylesheet/patches responses pass through the CSS safety pipeline
1804
1900
  * (force-scoped under html[data-dsh-skin="<id>"], whitelist fail-closed), so
1805
1901
  * the browser can inject them blindly. hooks.mjs is served verbatim — it is
1806
- * trusted, same-review same-release code (high sensitivity, see contracts/).
1902
+ * trusted, same-review same-release code (high sensitivity, see contracts/),
1903
+ * served for built-in skins and for user-directory skins whose install
1904
+ * provenance pins the bytes to the official DSH Market (issue #1073).
1807
1905
  * @module @linxin666/dsh-client-ui-skin-center/routes-v2
1808
1906
  */
1809
1907
  const SKIN_CENTER_V2_PREFIX = "/api/skin-center/v2";
@@ -1897,7 +1995,8 @@ function makeSkinCenterV2Routes(deps = {}) {
1897
1995
  skins: catalog.skins.filter((s) => s.origin === "user" || shippedSet.has(s.manifest.id)).map((s) => ({
1898
1996
  origin: s.origin,
1899
1997
  warnings: s.warnings,
1900
- manifest: s.manifest
1998
+ manifest: s.manifest,
1999
+ channel: s.origin === "user" ? existsSync(join(s.dir, "dsh-market.provenance.json")) ? "market" : "unknown" : "npm"
1901
2000
  })),
1902
2001
  diagnostics: catalog.diagnostics
1903
2002
  });
@@ -1940,7 +2039,7 @@ function makeSkinCenterV2Routes(deps = {}) {
1940
2039
  });
1941
2040
  return;
1942
2041
  }
1943
- if (entry.origin !== "builtin") {
2042
+ if (entry.origin !== "builtin" && entry.hooksTrusted !== true) {
1944
2043
  writeJson(res, 403, {
1945
2044
  ok: false,
1946
2045
  error: "hooks-require-review",
@@ -8589,7 +8688,7 @@ function makeWeRoutes(deps) {
8589
8688
  //#region src/mount-once.ts
8590
8689
  /**
8591
8690
  * Host single-instance guard shared by the plugin family. The family bundle
8592
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
8691
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
8593
8692
  * the loader accepts a standalone install of the same package side by side;
8594
8693
  * without this guard the second instance would still re-register the same
8595
8694
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -8602,7 +8701,7 @@ function makeWeRoutes(deps) {
8602
8701
  * `ctx.effect` runs its callback immediately and treats the callback's
8603
8702
  * return value as the fiber disposer, so the unmarker is returned, not run.
8604
8703
  */
8605
- const MOUNTED = Symbol.for("dsh-web-ui.mounted-plugins");
8704
+ const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
8606
8705
  function mountedSet() {
8607
8706
  const registry = globalThis;
8608
8707
  return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
@@ -8706,6 +8805,7 @@ const SkinWallpaperConfigSchema = z.object({
8706
8805
  pauseOnHidden: z.boolean().default(true),
8707
8806
  dim: z.number().min(0).max(90).step(5).default(25),
8708
8807
  wallpaperBlur: z.number().min(0).max(60).step(1).default(0),
8808
+ wallpaperOpacity: z.number().min(0).max(100).step(5).default(100),
8709
8809
  fit: z.union([
8710
8810
  "cover",
8711
8811
  "contain",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-skin-center",
3
3
  "description": "In-GUI skin center for the dsh web GUI — the single skin package: skins are pure asset directories (built-in plus $DSH_HOME/skins) loaded and rendered only by the skin center; live try-on and one-click apply switch atomically in the page (no reload, no cordis.patch.yml rewrite, no restart)",
4
- "version": "0.3.2",
4
+ "version": "0.3.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "repository": {
66
66
  "type": "git",
67
- "url": "https://github.com/zhu1090093659/dsh-web-ui.git"
67
+ "url": "https://github.com/zhu1090093659/dsh-web.git"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsdown",
@@ -4,7 +4,7 @@
4
4
  "id": "blue-fantasy",
5
5
  "name": "蓝色幻想",
6
6
  "nameEn": "Blue Fantasy",
7
- "author": "powerdog996(DreamSkin 社区)· dsh-web-ui 适配",
7
+ "author": "powerdog996(DreamSkin 社区)· dsh-web 适配",
8
8
  "tagline": "鲸鱼插画背景 · periwinkle 靛蓝调色板 · 半透明面板",
9
9
  "description": "DreamSkin「DeepSeek-鲸鱼娘」Codex 桌面主题的 dsh 适配:鲸鱼插画背景垫在半透明面板之下,遮罩随亮/暗主题实时切换,periwinkle 靛蓝色调重映射到全部 dsh token。",
10
10
  "tags": [