@pellux/goodvibes-sdk 0.19.9 → 0.21.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.
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- let version = '0.19.9';
3
+ let version = '0.21.1';
4
4
  try {
5
5
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf-8'));
6
6
  version = pkg.version ?? version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-sdk",
3
- "version": "0.19.9",
3
+ "version": "0.21.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/mgd34msu/goodvibes-sdk.git"
@@ -112,7 +112,8 @@
112
112
  "description": "TypeScript SDK for building GoodVibes operator, peer, web, mobile, and daemon-connected apps with typed contracts, auth, realtime events, and transport layers.",
113
113
  "files": [
114
114
  "dist",
115
- "sbom.cdx.json"
115
+ "sbom.cdx.json",
116
+ "scripts/postinstall-patch-minimatch.mjs"
116
117
  ],
117
118
  "homepage": "https://github.com/mgd34msu/goodvibes-sdk",
118
119
  "keywords": [
@@ -143,5 +144,11 @@
143
144
  "react-native-keychain": {
144
145
  "optional": true
145
146
  }
147
+ },
148
+ "overrides": {
149
+ "minimatch": "^10.2.5"
150
+ },
151
+ "scripts": {
152
+ "postinstall": "node scripts/postinstall-patch-minimatch.mjs"
146
153
  }
147
154
  }
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall-patch-minimatch.mjs
4
+ *
5
+ * Upgrades vulnerable minimatch transitive installs in consumer node_modules.
6
+ *
7
+ * Context: bash-language-server@5.6.0 hard-pins editorconfig@2.0.1, which
8
+ * hard-pins minimatch@10.0.1. npm/bun ignore overrides fields in published
9
+ * packages, so the root overrides in this SDK cannot fix consumer trees.
10
+ * This postinstall patcher reaches into the consumer's node_modules and
11
+ * upgrades any minimatch in the vulnerable range >=10.0.0 <10.2.3 in place.
12
+ *
13
+ * Advisory IDs addressed:
14
+ * GHSA-3ppc-4f35-3m26, GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74
15
+ *
16
+ * Node stdlib only. No third-party dependencies. Requires Node 18+.
17
+ */
18
+
19
+ import { createGunzip } from 'node:zlib';
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ readFileSync,
25
+ writeFileSync,
26
+ } from 'node:fs';
27
+ import { dirname, join, relative } from 'node:path';
28
+
29
+ const PATCH_VERSION = '10.2.5';
30
+ const REGISTRY_URL = `https://registry.npmjs.org/minimatch/-/minimatch-${PATCH_VERSION}.tgz`;
31
+
32
+ /**
33
+ * Parse semver string into major/minor/patch integers.
34
+ * @param {string} v
35
+ * @returns {{ major: number, minor: number, patch: number } | null}
36
+ */
37
+ function parseVersion(v) {
38
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
39
+ if (!m) return null;
40
+ return {
41
+ major: parseInt(m[1], 10),
42
+ minor: parseInt(m[2], 10),
43
+ patch: parseInt(m[3], 10),
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Returns true if version is in the vulnerable range: >=10.0.0 <10.2.3
49
+ * @param {string} v
50
+ * @returns {boolean}
51
+ */
52
+ function isVulnerable(v) {
53
+ const p = parseVersion(v);
54
+ if (!p) return false;
55
+ if (p.major !== 10) return false;
56
+ if (p.minor < 2) return true;
57
+ if (p.minor === 2 && p.patch < 3) return true;
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Minimal synchronous tar extractor. Handles ustar-compatible archives.
63
+ * Supports regular files and directories. Strips the leading "package/"
64
+ * prefix that npm tarballs use.
65
+ *
66
+ * @param {Buffer} tarball - raw (already gunzip-decoded) tar data
67
+ * @param {string} destDir - destination directory (the minimatch package root)
68
+ */
69
+ function extractTar(tarball, destDir) {
70
+ const BLOCK = 512;
71
+ let offset = 0;
72
+
73
+ while (offset + BLOCK <= tarball.length) {
74
+ const header = tarball.subarray(offset, offset + BLOCK);
75
+ offset += BLOCK;
76
+
77
+ // End-of-archive: two consecutive zero blocks.
78
+ if (header.every((b) => b === 0)) break;
79
+
80
+ const nameRaw = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
81
+ if (!nameRaw) break;
82
+
83
+ const prefixRaw = header.subarray(345, 500).toString('utf8').replace(/\0.*$/, '');
84
+ const fullName = prefixRaw ? `${prefixRaw}/${nameRaw}` : nameRaw;
85
+
86
+ const sizeOctal = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim();
87
+ const size = parseInt(sizeOctal, 8) || 0;
88
+
89
+ const typeFlag = String.fromCharCode(header[156]);
90
+ const isDir = typeFlag === '5';
91
+ const isFile = typeFlag === '0' || typeFlag === '\0';
92
+
93
+ const blocks = Math.ceil(size / BLOCK);
94
+ const fileData = isFile ? tarball.subarray(offset, offset + size) : null;
95
+ offset += blocks * BLOCK;
96
+
97
+ // Strip leading "package/" prefix (npm tarballs use "package/" as root).
98
+ let relPath = fullName;
99
+ if (relPath.startsWith('package/')) {
100
+ relPath = relPath.slice('package/'.length);
101
+ } else if (relPath === 'package') {
102
+ continue;
103
+ }
104
+
105
+ // Safety: reject absolute paths and path traversal.
106
+ if (!relPath || relPath.startsWith('/') || relPath.includes('..')) continue;
107
+
108
+ const destPath = join(destDir, relPath);
109
+
110
+ if (isDir || relPath.endsWith('/')) {
111
+ mkdirSync(destPath, { recursive: true });
112
+ } else if (isFile && fileData !== null) {
113
+ mkdirSync(dirname(destPath), { recursive: true });
114
+ writeFileSync(destPath, fileData);
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Download and gunzip the minimatch tgz from npm registry.
121
+ * Returns the raw tar buffer (decompressed).
122
+ * @returns {Promise<Buffer>}
123
+ */
124
+ async function fetchTarball() {
125
+ const resp = await fetch(REGISTRY_URL);
126
+ if (!resp.ok) {
127
+ throw new Error(`fetch ${REGISTRY_URL} → ${resp.status} ${resp.statusText}`);
128
+ }
129
+ const compressed = Buffer.from(await resp.arrayBuffer());
130
+
131
+ return new Promise((resolve, reject) => {
132
+ const gunzip = createGunzip();
133
+ /** @type {Buffer[]} */
134
+ const chunks = [];
135
+ gunzip.on('data', (chunk) => chunks.push(/** @type {Buffer} */ (chunk)));
136
+ gunzip.on('end', () => resolve(Buffer.concat(chunks)));
137
+ gunzip.on('error', reject);
138
+ gunzip.end(compressed);
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Walk node_modules and return paths to every minimatch/package.json found.
144
+ * Covers three layouts:
145
+ * - flat: node_modules/minimatch/package.json
146
+ * - nested: node_modules/foo/node_modules/minimatch/package.json
147
+ * - pnpm: node_modules/.pnpm/minimatch@X/node_modules/minimatch/package.json
148
+ *
149
+ * @param {string} nodeModules - absolute path to the node_modules root
150
+ * @returns {string[]}
151
+ */
152
+ function findMinimatchPaths(nodeModules) {
153
+ /** @type {string[]} */
154
+ const results = [];
155
+
156
+ /** @param {string} dir */
157
+ function scan(dir) {
158
+ /** @type {string[]} */
159
+ let entries;
160
+ try {
161
+ entries = readdirSync(dir);
162
+ } catch {
163
+ return;
164
+ }
165
+
166
+ for (const entry of entries) {
167
+ // Skip hidden dirs except .pnpm (pnpm virtual store).
168
+ if (entry.startsWith('.') && entry !== '.pnpm') continue;
169
+
170
+ const full = join(dir, entry);
171
+
172
+ if (entry === 'minimatch') {
173
+ const pkgJson = join(full, 'package.json');
174
+ if (existsSync(pkgJson)) results.push(pkgJson);
175
+ // Don't recurse into minimatch's own directory.
176
+ continue;
177
+ }
178
+
179
+ if (entry === '.pnpm') {
180
+ // pnpm isolated installs: .pnpm/<name@version>/node_modules/<name>
181
+ /** @type {string[]} */
182
+ let pnpmEntries;
183
+ try {
184
+ pnpmEntries = readdirSync(full);
185
+ } catch {
186
+ continue;
187
+ }
188
+ for (const pnpmEntry of pnpmEntries) {
189
+ // Check for minimatch directly in this pnpm store entry.
190
+ const pnpmMinimatch = join(full, pnpmEntry, 'node_modules', 'minimatch');
191
+ const pkgJson = join(pnpmMinimatch, 'package.json');
192
+ if (existsSync(pkgJson)) results.push(pkgJson);
193
+ // Also scan nested node_modules inside pnpm store entries.
194
+ const nested = join(full, pnpmEntry, 'node_modules');
195
+ if (existsSync(nested)) scan(nested);
196
+ }
197
+ continue;
198
+ }
199
+
200
+ // Scoped namespace (@scope) — recurse one level.
201
+ if (entry.startsWith('@')) {
202
+ scan(full);
203
+ continue;
204
+ }
205
+
206
+ // Regular package — check for nested node_modules.
207
+ const nested = join(full, 'node_modules');
208
+ if (existsSync(nested)) scan(nested);
209
+ }
210
+ }
211
+
212
+ scan(nodeModules);
213
+ return results;
214
+ }
215
+
216
+ async function main() {
217
+ // npm sets INIT_CWD to the consumer project root during postinstall.
218
+ // Fallback to process.cwd() which is the package root during npm install.
219
+ const consumerRoot = process.env.INIT_CWD ?? process.cwd();
220
+ const nodeModules = join(consumerRoot, 'node_modules');
221
+
222
+ if (!existsSync(nodeModules)) {
223
+ console.log('[pellux-patch] node_modules not found, skipping minimatch patch');
224
+ process.exit(0);
225
+ }
226
+
227
+ /** @type {string[]} */
228
+ let pkgJsonPaths;
229
+ try {
230
+ pkgJsonPaths = findMinimatchPaths(nodeModules);
231
+ } catch (err) {
232
+ console.warn(
233
+ `[pellux-patch] warning: failed to scan node_modules: ${/** @type {Error} */ (err)?.message ?? err}`,
234
+ );
235
+ process.exit(0);
236
+ }
237
+
238
+ /** @type {Array<{ pkgJsonPath: string, version: string, dir: string }>} */
239
+ const vulnerable = [];
240
+
241
+ for (const pkgJsonPath of pkgJsonPaths) {
242
+ try {
243
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
244
+ const ver = /** @type {string} */ (pkg.version ?? '');
245
+ if (isVulnerable(ver)) {
246
+ vulnerable.push({ pkgJsonPath, version: ver, dir: dirname(pkgJsonPath) });
247
+ }
248
+ } catch {
249
+ // Unreadable package.json — skip silently.
250
+ }
251
+ }
252
+
253
+ if (vulnerable.length === 0) {
254
+ console.log('[pellux-patch] no vulnerable minimatch detected');
255
+ process.exit(0);
256
+ }
257
+
258
+ /** @type {Buffer | null} */
259
+ let tarball = null;
260
+
261
+ for (const { pkgJsonPath, version, dir } of vulnerable) {
262
+ const rel = relative(consumerRoot, pkgJsonPath);
263
+ try {
264
+ if (tarball === null) {
265
+ tarball = await fetchTarball();
266
+ }
267
+ extractTar(tarball, dir);
268
+ console.log(`[pellux-patch] upgraded minimatch at ${rel}: ${version} → ${PATCH_VERSION}`);
269
+ } catch (err) {
270
+ // Never fail the consumer's install — log and continue.
271
+ console.warn(
272
+ `[pellux-patch] warning: failed to patch ${rel}: ${/** @type {Error} */ (err)?.message ?? err}`,
273
+ );
274
+ }
275
+ }
276
+ }
277
+
278
+ main().catch((err) => {
279
+ // Belt-and-suspenders: catch any top-level unhandled rejection and warn,
280
+ // never propagate — postinstall must never break a consumer install.
281
+ console.warn(
282
+ `[pellux-patch] warning: postinstall patcher encountered an error: ${/** @type {Error} */ (err)?.message ?? err}`,
283
+ );
284
+ process.exit(0);
285
+ });