gyoshu 0.4.20 → 0.4.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gyoshu",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "Scientific research agent extension for OpenCode - turns research goals into reproducible Jupyter notebooks",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -20,7 +20,7 @@
20
20
  * @module checkpoint-schema
21
21
  */
22
22
 
23
- import { z } from "zod/v4";
23
+ import { z } from "zod";
24
24
 
25
25
  // =============================================================================
26
26
  // ENUM TYPES
package/src/lib/paths.ts CHANGED
@@ -46,7 +46,15 @@ import * as fs from "fs";
46
46
  import * as path from "path";
47
47
  import * as os from "os";
48
48
  import * as crypto from "crypto";
49
- import { readFileNoFollowSync } from "./atomic-write";
49
+
50
+ function readFileNoFollowSyncLocal(filePath: string): string {
51
+ const fd = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
52
+ try {
53
+ return fs.readFileSync(fd, "utf-8");
54
+ } finally {
55
+ fs.closeSync(fd);
56
+ }
57
+ }
50
58
 
51
59
  // =============================================================================
52
60
  // CONSTANTS
@@ -812,8 +820,7 @@ export function getLegacyArtifactsDir(sessionId: string): string {
812
820
  export function getConfig(): GyoshuConfig | null {
813
821
  const configPath = getConfigPath();
814
822
  try {
815
- // Security: Use O_NOFOLLOW to atomically reject symlinks (no TOCTOU race)
816
- const content = readFileNoFollowSync(configPath);
823
+ const content = readFileNoFollowSyncLocal(configPath);
817
824
  return JSON.parse(content) as GyoshuConfig;
818
825
  } catch (err) {
819
826
  // ENOENT = doesn't exist, ELOOP = symlink rejected by O_NOFOLLOW
@@ -15,8 +15,14 @@ import * as fs from "fs/promises";
15
15
  import { realpathSync } from "fs";
16
16
  import * as os from "os";
17
17
  import * as path from "path";
18
- import sanitizeHtmlLib from "sanitize-html";
19
18
  import { atomicReplaceWindows, durableAtomicWrite, readFileNoFollow } from "./atomic-write";
19
+
20
+ let sanitizeHtmlLib: ((html: string, options: object) => string) | null = null;
21
+ try {
22
+ sanitizeHtmlLib = require("sanitize-html");
23
+ } catch {
24
+ // Package not available - will use fallback sanitization
25
+ }
20
26
  import { isPathContainedIn } from "./path-security";
21
27
  import { ensureDirSync, getReportsRootDir } from "./paths";
22
28
 
@@ -356,34 +362,36 @@ async function convertWithHtml(
356
362
  * Only permits safe structural tags with no URL-bearing attributes.
357
363
  */
358
364
  function sanitizeHtml(html: string): string {
359
- return sanitizeHtmlLib(html, {
360
- // Allowlist: only safe structural and formatting tags
361
- allowedTags: [
362
- "h1", "h2", "h3", "h4", "h5", "h6",
363
- "p", "br", "hr",
364
- "ul", "ol", "li",
365
- "blockquote", "pre", "code",
366
- "table", "thead", "tbody", "tr", "th", "td",
367
- "strong", "em", "b", "i", "u", "s",
368
- "span", "div",
369
- "a", // Keep anchor structure but strip href below
370
- ],
371
- // Allowlist attributes - NO URL-bearing attributes allowed
372
- allowedAttributes: {
373
- a: [], // Remove all attributes from links (no href = no SSRF)
374
- th: ["colspan", "rowspan"],
375
- td: ["colspan", "rowspan"],
376
- "*": ["class"], // Allow class for styling only
377
- },
378
- // Disallow ALL URL schemes - prevents any protocol-based requests
379
- allowedSchemes: [],
380
- allowedSchemesByTag: {},
381
- // Remove ALL inline styles - prevents url() injection
382
- allowedStyles: {},
383
- // Strip unknown tags completely
384
- disallowedTagsMode: "discard",
385
- allowProtocolRelative: false,
386
- });
365
+ if (sanitizeHtmlLib) {
366
+ return sanitizeHtmlLib(html, {
367
+ allowedTags: [
368
+ "h1", "h2", "h3", "h4", "h5", "h6",
369
+ "p", "br", "hr",
370
+ "ul", "ol", "li",
371
+ "blockquote", "pre", "code",
372
+ "table", "thead", "tbody", "tr", "th", "td",
373
+ "strong", "em", "b", "i", "u", "s",
374
+ "span", "div",
375
+ "a",
376
+ ],
377
+ allowedAttributes: {
378
+ a: [],
379
+ th: ["colspan", "rowspan"],
380
+ td: ["colspan", "rowspan"],
381
+ "*": ["class"],
382
+ },
383
+ allowedSchemes: [],
384
+ allowedSchemesByTag: {},
385
+ allowedStyles: {},
386
+ disallowedTagsMode: "discard",
387
+ allowProtocolRelative: false,
388
+ });
389
+ }
390
+ return html
391
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
392
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
393
+ .replace(/\s(on\w+|href|src|style)="[^"]*"/gi, "")
394
+ .replace(/\s(on\w+|href|src|style)='[^']*'/gi, "");
387
395
  }
388
396
 
389
397
  /**