@groeponline/pi-wishcraft 0.23.2 → 0.23.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.23.3] - 2026-08-20
6
+
7
+ ### Fixed
8
+ - Nested settings writes reject `__proto__` / `constructor` / `prototype` keys in the assignment loop.
9
+ - Test workflow pins `GITHUB_TOKEN` to `contents: read`.
10
+
5
11
  ## [0.23.2] - 2026-08-20
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "0.23.2",
3
+ "version": "0.23.3",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
@@ -55,7 +55,44 @@ export function readConfigPath(settings: Record<string, unknown>, path: string):
55
55
  return null;
56
56
  }
57
57
 
58
- /** Genest schrijven (immutabel op elk niveau) + persist naar settings.json. */
58
+ /** True for path segments that would mutate Object.prototype. */
59
+ export function isUnsafeConfigKey(key: string): boolean {
60
+ return key === "__proto__" || key === "constructor" || key === "prototype";
61
+ }
62
+
63
+ /**
64
+ * Set `parts` (after the root key) on `root`. Returns false and leaves
65
+ * `root` unchanged when a segment is `__proto__`, `constructor`, or `prototype`.
66
+ */
67
+ export function assignNestedConfigValue(
68
+ root: Record<string, unknown>,
69
+ parts: string[],
70
+ value: ConfigValue,
71
+ ): boolean {
72
+ for (const part of parts) {
73
+ if (part === "__proto__" || part === "constructor" || part === "prototype") {
74
+ return false;
75
+ }
76
+ }
77
+ let node = root;
78
+ for (let i = 0; i < parts.length; i++) {
79
+ const key = parts[i]!;
80
+ // Guard in this loop so CodeQL sees the key check next to the assignment.
81
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
82
+ return false;
83
+ }
84
+ if (i === parts.length - 1) {
85
+ if (value === null) delete node[key];
86
+ else node[key] = value;
87
+ } else {
88
+ if (!isRecord(node[key])) node[key] = {};
89
+ node = node[key] as Record<string, unknown>;
90
+ }
91
+ }
92
+ return true;
93
+ }
94
+
95
+ /** Nested write (new object per level) + persist to settings.json. */
59
96
  export function writeConfigPath(
60
97
  cwd: string,
61
98
  path: string,
@@ -63,29 +100,20 @@ export function writeConfigPath(
63
100
  ): boolean {
64
101
  const parts = path.split(".");
65
102
  const rootKey = parts[0]!;
66
- // Weiger prototype-pollution keys (CodeQL).
67
- if (parts.some((p) => p === "__proto__" || p === "constructor" || p === "prototype")) {
103
+ if (!rootKey || parts.some(isUnsafeConfigKey)) {
68
104
  return false;
69
105
  }
70
106
  return writeSettingKey(cwd, rootKey, (existing) => {
71
- // Shorthand string onder powerline (bv. "chef") is een preset-naam: bewaar 'm.
72
- let node: Record<string, unknown> = isRecord(existing)
107
+ // Shorthand string under powerline (e.g. "chef") is a preset name: keep it.
108
+ const node: Record<string, unknown> = isRecord(existing)
73
109
  ? existing
74
110
  : rootKey === "powerline" && typeof existing === "string"
75
111
  ? { preset: existing }
76
112
  : {};
77
- const root = node;
78
- for (let i = 1; i < parts.length; i++) {
79
- const key = parts[i]!;
80
- if (i === parts.length - 1) {
81
- if (value === null) delete node[key];
82
- else node[key] = value;
83
- } else {
84
- if (!isRecord(node[key])) node[key] = {};
85
- node = node[key] as Record<string, unknown>;
86
- }
113
+ if (!assignNestedConfigValue(node, parts.slice(1), value)) {
114
+ return existing;
87
115
  }
88
- return root;
116
+ return node;
89
117
  });
90
118
  }
91
119