---
phase: 49-module-hardening
plan: 002
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/plugin/main.js
autonomous: true
requirements:
  - HARDEN-04
  - HARDEN-05

must_haves:
  truths:
    - "Obsidian plugin spawns OCR subprocess with PADDLEOCR_API_TOKEN in environment variable (not CLI argument) — API key not visible in Task Manager process list"
    - "Plugin directory tree renders via createEl() DOM API (not innerHTML assignment) — no XSS vector from user-configured directory names containing HTML/script tags"
  artifacts:
    - path: paperforge/plugin/main.js
      provides: "Env-based API key passing and createEl() DOM rendering"
      min_lines: 1
  key_links:
    - from: main.js:_runInstall()
      to: "spawn(..., { env: { ...process.env, PADDLEOCR_API_TOKEN } })"
      via: "env property in spawn options"
      pattern: "PADDLEOCR_API_TOKEN"
    - from: main.js:_stepOverview()
      to: "createEl() API chain"
      via: "DOM API instead of innerHTML"
      pattern: "createEl\\(.*paperforge-dir"
---

<objective>
Harden the Obsidian plugin (`main.js`) with two safety measures: pass the PaddleOCR API key through environment variables instead of CLI arguments (HARDEN-04), and replace `innerHTML` DOM assignment with `createEl()` DOM API calls to eliminate XSS vectors (HARDEN-05).

Purpose: Prevent API key leakage via process listing (Task Manager / ps), and prevent XSS injection through user-configured directory names.

Output:
  - Modified `paperforge/plugin/main.js` with both hardening measures
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md

<interfaces>
From paperforge/plugin/main.js (current spawn pattern, line 2088-2093):
```js
const runPython = (args, options = {}) => new Promise((resolve, reject) => {
    const child = spawn('python', args, {
        cwd: s.vault_path.trim(),
        env: process.env,         // base env, overridden by ...options
        timeout: 120000,
        ...options,               // options.env replaces env above if provided
    });
    ...
});
```

Current setupArgs (lines 2112-2123):
```js
const setupArgs = [
    '-m', 'paperforge',
    '--vault', s.vault_path.trim(),
    'setup', '--headless',
    '--paddleocr-key', s.paddleocr_api_key.trim(),  // <-- HARDEN-04: REMOVE
    '--system-dir', s.system_dir.trim(),
    '--resources-dir', s.resources_dir.trim(),
    '--literature-dir', s.literature_dir.trim(),
    '--control-dir', s.control_dir.trim(),
    '--base-dir', s.base_dir.trim(),
    '--agent', s.agent_platform || 'opencode',
];
```

Current innerHTML block (lines 1815-1825):
```js
tree.innerHTML = `
    <div class="paperforge-dir-node root">📁 Vault (${vault})</div>
    <div class="paperforge-dir-children">
        <div class="paperforge-dir-node folder">📁 ${s.resources_dir || 'Resources'}/ — description
            <div class="paperforge-dir-children">
                <div class="paperforge-dir-node file">📁 ${s.literature_dir || 'Literature'}/ — description</div>
            </div>
        </div>
        <div class="paperforge-dir-node folder">📁 ${s.base_dir || 'Bases'}/ — description</div>
        <div class="paperforge-dir-node folder">📁 ${s.system_dir || 'System'}/ — description</div>
    </div>`;
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Pass PADDLEOCR_API_TOKEN via environment variable, not CLI argument (HARDEN-04)</name>
  <files>paperforge/plugin/main.js</files>
  <action>
    === Change 1: Remove --paddleocr-key from setupArgs ===
    
    Lines 2112-2123: Remove the line `'--paddleocr-key', s.paddleocr_api_key.trim(),` from the `setupArgs` array.
    
    Old:
    ```js
    const setupArgs = [
        '-m', 'paperforge',
        '--vault', s.vault_path.trim(),
        'setup', '--headless',
        '--paddleocr-key', s.paddleocr_api_key.trim(),
        '--system-dir', s.system_dir.trim(),
        ...
    ];
    ```
    
    New:
    ```js
    const setupArgs = [
        '-m', 'paperforge',
        '--vault', s.vault_path.trim(),
        'setup', '--headless',
        '--system-dir', s.system_dir.trim(),
        ...
    ];
    ```
    
    === Change 2: Pass API key via environment in spawn() ===
    
    Line 2144: Change the `runPython` call to pass the API token in the environment.
    
    Old:
    ```js
    await runPython(setupArgs, { logStdout: true });
    ```
    
    New:
    ```js
    await runPython(setupArgs, {
        logStdout: true,
        env: { ...process.env, PADDLEOCR_API_TOKEN: s.paddleocr_api_key.trim() },
    });
    ```
    
    Note: The `env` property in options overrides the default `env: process.env` from `runPython`'s base spawn options. Spreading `...process.env` preserves all existing environment variables, and `PADDLEOCR_API_TOKEN` adds the API key without exposing it in the process command line.
  </action>
  <verify>
    <automated>cd /D "D:\L\Med\Research\99_System\LiteraturePipeline\github-release" && node -e "const fs=require('fs');const c=fs.readFileSync('paperforge/plugin/main.js','utf-8');console.log('--paddleocr-key present:',c.includes('--paddleocr-key'));console.log('PADDLEOCR_API_TOKEN present:',c.includes('PADDLEOCR_API_TOKEN'));process.exit(c.includes('--paddleocr-key')?1:0)"</automated>
  </verify>
  <done>
    - `--paddleocr-key` no longer appears in main.js
    - `PADDLEOCR_API_TOKEN` appears in main.js in the env block
    - No CLI argument exposes the API key for process listing
  </done>
</task>

<task type="auto">
  <name>Task 2: Replace innerHTML with createEl() DOM API for directory tree (HARDEN-05)</name>
  <files>paperforge/plugin/main.js</files>
  <action>
    Lines 1815-1825: Replace the innerHTML template literal with createEl() DOM API calls.
    
    Replace this block (lines 1815-1825):
    ```js
    tree.innerHTML = `
        <div class="paperforge-dir-node root">📁 Vault (${vault})</div>
        <div class="paperforge-dir-children">
            <div class="paperforge-dir-node folder">📁 ${s.resources_dir || 'Resources'}/ — 文献卡片目录（Base 数据来源）
                <div class="paperforge-dir-children">
                    <div class="paperforge-dir-node file">📁 ${s.literature_dir || 'Literature'}/ — 文献卡片</div>
                </div>
            </div>
            <div class="paperforge-dir-node folder">📁 ${s.base_dir || 'Bases'}/ — 数据管理面板</div>
            <div class="paperforge-dir-node folder">📁 ${s.system_dir || 'System'}/ — Zotero 软链接 + PaperForge 系统文件夹</div>
        </div>`;
    ```
    
    With this createEl() chain:
    ```js
    // HARDEN-05: Use createEl() DOM API instead of innerHTML to prevent XSS
    // from user-configured directory names containing HTML/script tags.
    const rootNode = tree.createEl('div', { cls: 'paperforge-dir-node root' });
    rootNode.textContent = `📁 Vault (${vault})`;
    
    const children = tree.createEl('div', { cls: 'paperforge-dir-children' });
    
    const resourcesFolder = children.createEl('div', { cls: 'paperforge-dir-node folder' });
    resourcesFolder.textContent = `📁 ${s.resources_dir || 'Resources'}/ — 文献卡片目录（Base 数据来源）`;
    const resourcesChildren = resourcesFolder.createEl('div', { cls: 'paperforge-dir-children' });
    resourcesChildren.createEl('div', { cls: 'paperforge-dir-node file',
        text: `📁 ${s.literature_dir || 'Literature'}/ — 文献卡片` });
    
    children.createEl('div', { cls: 'paperforge-dir-node folder',
        text: `📁 ${s.base_dir || 'Bases'}/ — 数据管理面板` });
    children.createEl('div', { cls: 'paperforge-dir-node folder',
        text: `📁 ${s.system_dir || 'System'}/ — Zotero 软链接 + PaperForge 系统文件夹` });
    ```
    
    Key points:
    - `createEl()` appends the new element to the parent (same as `el.appendChild(document.createElement(...))`)
    - `textContent` (or `createEl`'s `text` property) is used instead of `innerHTML`, which means user-supplied directory names like `<script>alert('xss')</script>` are rendered as literal text, not executed
    - The `text` property on `createEl()` sets `textContent` internally, same protection
    - The visual tree structure is preserved exactly: same CSS classes, same content
  </action>
  <verify>
    <automated>cd /D "D:\L\Med\Research\99_System\LiteraturePipeline\github-release" && node -e "const fs=require('fs');const c=fs.readFileSync('paperforge/plugin/main.js','utf-8');console.log('innerHTML present:',c.includes('tree.innerHTML='));console.log('createEl dir-tree present:',c.includes('paperforge-dir-node'));process.exit(c.includes('tree.innerHTML=')?1:0)"</automated>
  </verify>
  <done>
    - `tree.innerHTML` no longer appears in main.js (directory tree section)
    - Directory tree renders via `createEl()` chain
    - XSS vector from user-configured directory names eliminated
  </done>
</task>

</tasks>

<verification>
1. Verify no `tree.innerHTML` exists in paperforge/plugin/main.js — grep returns zero hits
2. Verify `PADDLEOCR_API_TOKEN` env var pattern present in spawn() call
3. Verify `--paddleocr-key` no longer appears in main.js
</verification>

<success_criteria>
- [ ] `paperforge/plugin/main.js` passes PADDLEOCR_API_TOKEN via `env:` in spawn(), not `--paddleocr-key` CLI arg
- [ ] `paperforge/plugin/main.js` renders directory tree via `createEl()` (no `innerHTML` in that section)
- [ ] No XSS vector from user-configured directory names containing HTML/script tags
- [ ] No API key exposure in process list
</success_criteria>

<output>
After completion, create `.planning/phases/49-module-hardening/49-002-SUMMARY.md`
</output>