docs: update human docs for 1.0.11 and the redaction lint gate

STATUS.md current-focus and counts refreshed; DECISIONS.md gains the
no-unredacted-console gate (#418) and automated-review triage (#454)
entries; ARCHITECTURE.md security layers and audit status extended.

Co-Authored-By: Claude <bot@wafflenet.io>
This commit is contained in:
bot 2026-07-03 14:41:26 -07:00
parent 5cd534b5e0
commit 0e6f50ba3f
No known key found for this signature in database
3 changed files with 47 additions and 10 deletions

View file

@ -1,6 +1,6 @@
# Architecture Overview
**Last updated**: 2026-07-02 · **Version**: 1.0.10
**Last updated**: 2026-07-03 · **Version**: 1.0.11
Synapse is an Obsidian plugin that layers AI-powered features over a vault: note elaboration (with image analysis), audio transcription, video transcription, image OCR, note enrichment, summarization, note tidying, semantic organization, recursive deep-dive note generation, title proposals, and in-place wikilink discovery (REM). Two coordination layers tie them together — a **Fire Synapse pipeline** that runs the features in a fixed order over a folder or note, and an **intake** watcher that auto-processes notes dropped into an inbox. It runs on both desktop and mobile (video and media-clipping features are desktop-only).
@ -940,6 +940,7 @@ Path exclusion is centralized (#307): the per-module `excludeFolders` fields wer
| Temp-path hardening | Vault-derived basenames sanitized before composing temp paths (2026-06-08) | `transcription/duration-detector.ts` |
| Multipart header hardening | Vault-/settings-derived field + file names sanitized (`sanitizeMultipartHeaderValue`: strip CR/LF, replace `"`/`\`) before `Content-Disposition` lines — blocks multipart/header injection (2026-06-11) | `audio/transcriber.ts` |
| API key protection | `redactSecrets()` (strings) + `redactError()` (raw caught errors — Error `.stack`/`.message`) — **single source of truth** scrubbing keys from error messages/console on **every error path**; covers OpenAI/Anthropic `sk-`, `key-`, Deepgram `dg-`, `Bearer`/`Token`, `anthropic-`, and Gemini `AIza`. Password-masked inputs; keys live only in gitignored `data.json` | `shared/redact.ts` (used by `ai-client.ts`, `credential-validator.ts`, all of `notifications.ts`; `redactError` at audio/index, rem/semantic-matcher, elaboration/image-analyzer + proposer, image/preprocess, shared/fire-and-forget, the clipboard-copy catches in `notifications.ts` + `video/settings-section.ts`, `main.ts` lifecycle paths, and `update-checker.ts`; `redactSecrets` also at the credential Test chip `credential-field.ts` and the `update-checker.ts` fetch-fail log) |
| Redaction enforcement (lint) | Custom type-aware ESLint rule `synapse/no-unredacted-console` (#418): every value reaching a `console.*` sink must be **statically string-like** — i.e. already rendered through `redactError`/`redactSecrets`, which return `string`. Flags `String()`/`JSON.stringify()` of non-strings and direct `.message`/`.stack` access as bypasses; fails closed if type info is unavailable; scoped to shipped `src` (tests/mocks excluded). Runs in CI via `npm run lint` | `scripts/eslint-rules/no-unredacted-console.mjs`, `eslint.config.mjs` |
| Prompt-injection defense | `wrapUntrusted()` fences fetched untrusted content (article/tweet/Reddit, image analysis) in labeled delimiters + data-not-instructions frame + anti-breakout scrub — structural, not lexical (#398); Gemini audio instruction in `system_instruction` | `shared/untrusted-content.ts` (used by `elaboration/proposer.ts`), `audio/transcriber.ts` |
| Frontmatter safety | Key validation regex + forbidden keys blocklist | `enrichment/enrichment-applier.ts` |
| Network security | Ollama HTTPS required (HTTP for localhost only), 2min timeouts | `shared/ai-client.ts` |
@ -948,7 +949,7 @@ Path exclusion is centralized (#307): the per-module `excludeFolders` fields wer
| Prototype pollution | `deepMerge` skips `__proto__`, `constructor`, `prototype` keys | `main.ts` |
| Lifecycle hygiene | `NotificationManager.dispose()` tears down in-flight ellipsis timers + hides notices on unload (no orphaned `setInterval`) | `shared/notifications.ts`, `main.ts:onunload()` |
**Audit status:** The full audit (2026-06-08) found no critical or high vulnerabilities. The 2026-06-11 re-audit added two defense-in-depth hardenings — canonical secret redaction (now covering Gemini `AIza` keys everywhere) and multipart header-injection hardening. The 2026-06-20 audit pass re-verified architecture, security, and Obsidian-guideline compliance as clean; its one fix was a lifecycle leak (in-flight notification ellipsis timers now torn down on unload). The 2026-06-25 audit pass (v1.0.6) brought the per-operation error `console.error` under `redactSecrets`, so the single redaction source now guards **every** error sink in `notifications.ts`. The 2026-06-29 audit pass (v1.0.7) added `redactError()` so raw caught errors logged directly to the console get the same scrub (five direct error sinks routed through it); the idempotency bundle also landed a structural prompt-injection fence (`wrapUntrusted`, #398) for fetched external content. The 2026-07-02 audit pass (v1.0.10) completed the `redactError` rollout — the **remaining** direct error-log call sites (`main.ts` lifecycle paths, the update checker, the credential Test chip, the image-downscale fallback, and the two clipboard-copy catches) now route through the scrub — and rerouted the `title` module's back-compat re-export of `isUntitled` through the `../shared` barrel instead of the internal `shared/title-detector` file (barrel-import rule; the canonical home has been `shared/` since #387). `data.json` (live API keys) is gitignored and never committed.
**Audit status:** The full audit (2026-06-08) found no critical or high vulnerabilities. The 2026-06-11 re-audit added two defense-in-depth hardenings — canonical secret redaction (now covering Gemini `AIza` keys everywhere) and multipart header-injection hardening. The 2026-06-20 audit pass re-verified architecture, security, and Obsidian-guideline compliance as clean; its one fix was a lifecycle leak (in-flight notification ellipsis timers now torn down on unload). The 2026-06-25 audit pass (v1.0.6) brought the per-operation error `console.error` under `redactSecrets`, so the single redaction source now guards **every** error sink in `notifications.ts`. The 2026-06-29 audit pass (v1.0.7) added `redactError()` so raw caught errors logged directly to the console get the same scrub (five direct error sinks routed through it); the idempotency bundle also landed a structural prompt-injection fence (`wrapUntrusted`, #398) for fetched external content. The 2026-07-02 audit pass (v1.0.10) completed the `redactError` rollout — the **remaining** direct error-log call sites (`main.ts` lifecycle paths, the update checker, the credential Test chip, the image-downscale fallback, and the two clipboard-copy catches) now route through the scrub — and rerouted the `title` module's back-compat re-export of `isUntitled` through the `../shared` barrel instead of the internal `shared/title-detector` file (barrel-import rule; the canonical home has been `shared/` since #387). The 2026-07-03 audit pass (v1.0.11) re-verified architecture and security as clean and made the redaction contract **regression-proof**: the custom `synapse/no-unredacted-console` lint rule (#418) now fails CI on any console sink that isn't statically string-like. Separately, the v1.0.11 store automated-review findings were triaged into a reviewer-facing `docs/automated-review-notes.md` (#454) — its one code fix replaced summarize's deprecated `querySelectorAll` usage with Obsidian's typed `containerEl.findAll()`. `data.json` (live API keys) is gitignored and never committed.
**Known posture / not-yet-enforced:**
- `ensureWithinVault()` exists in `shared/validation.ts` but is **not yet wired into write paths** — there is no active vault-boundary enforcement on writes today.
@ -966,5 +967,5 @@ Path exclusion is centralized (#307): the per-module `excludeFolders` fields wer
6. All shared utilities imported from `../shared` (barrel export) — never from a sibling feature module or an internal `shared/` file. Where you only need a *type* from a higher layer, use `import type` (erased at compile time, no runtime cycle).
7. Need a Node builtin (`fs`/`path`/`os`/`child_process`)? Go through `loadNodeModules()`/`assertDesktop()` only — never `require` at module top level (keeps the bundle mobile-loadable).
8. Gate vault paths with `isPathExcluded(path, featureId, settings)` and tags with the shared `matchesExcludeTag` helper.
9. Build check: `npm run build` (type-checks + bundles); tests: `npm test`
9. Build check: `npm run build` (type-checks + bundles); tests: `npm test`; lint: `npm run lint` (Obsidian store-review rules + the redaction console-sink gate, #418)
10. Git: create a feature branch, push, open PR. See `.claude/skills/git-workflow/SKILL.md` for full protocol.

View file

@ -4,6 +4,39 @@ Decisions listed in reverse chronological order.
---
## 2026-07-03: The console-sink redaction contract is now lint-enforced (`synapse/no-unredacted-console`, #418)
**Context**: Three successive audit passes (2026-06-25, 2026-06-29, 2026-07-02) each found and patched console sinks that logged unredacted values — the contract "every error console sink routes through `redactError`/`redactSecrets`" was held only by convention, and nothing stopped a new `console.error('…', err)` from silently reopening the hole. The sinks listed in #418 were already fixed by the 2026-07-02 pass; the remaining deliverable was an enforcement guard.
**Decision**: Add a custom **type-aware** ESLint rule, `synapse/no-unredacted-console` (`scripts/eslint-rules/no-unredacted-console.mjs`, registered in `eslint.config.mjs` and run by `npm run lint` in CI). Every value reaching a `console.*` call must be **statically string-like** — checked at the argument top level, inside template-literal substitutions, and across `+` concatenation. Because `redactError()`/`redactSecrets()` return `string`, sanctioned call sites pass with **no function-name allowlist**. `String()`/`JSON.stringify()` of non-string values and direct `.message`/`.stack` access are flagged as bypasses (stringification is not redaction). The rule **fails closed** (throws) if type information is unavailable. Scoped to shipped code only — `src/**/*.ts` minus tests, mocks, and test-utils, mirroring the `obsidianmd/*` lint scope (test infra isn't bundled).
**Alternatives considered**:
- **Allowlist sanctioned wrapper names** (`redactError`, `redactSecrets`) — rejected; a rename or a new helper silently escapes the list, while "returns `string`" needs no list and covers future helpers automatically.
- **Non-type-aware AST pattern matching** — rejected; without the type checker the rule can't tell a plain string variable from an `Error`, so it would either over-flag safe sites or under-protect.
- **Keep catching regressions in periodic audits** — rejected; three audits in a row patching the same class of gap is exactly the signal that a convention needs a machine gate.
**Rationale**: The redaction guarantee only holds if *every* console path goes through the scrub — a property that must be enforced continuously, not re-verified per audit. Framing the rule as "only strings reach the console" enforces the **outcome** (a scrubbed, rendered string) rather than a spelling, so it can't be satisfied by accident or dodged by a rename.
**Impact**: New `scripts/eslint-rules/no-unredacted-console.mjs`; `eslint.config.mjs`. CI-only — no runtime behavior change; a regressed sink now fails `npm run lint`. Closes the enforcement gap behind the 2026-06-29 and 2026-07-02 redaction entries below.
---
## 2026-07-03: Automated store-review findings get a triaged, reviewer-facing companion doc (#454)
**Context**: Obsidian auto-reviews **every** GitHub release of a listed plugin (see the 2026-06-28 release-automation entry). The v1.0.11 run produced a findings dump mixing one genuine issue, several intentional-by-design patterns, false positives, and a large `no-unsafe-*` cascade that turned out to be an artifact of **untyped** analysis (the type-aware `npm run lint` exits 0 on the same code).
**Decision**: Triage every finding into one of four buckets — **fix**, **document-as-intended**, **rebut (false positive)**, or **untyped-analysis artifact** — and record the outcomes in a new reviewer-facing doc, `docs/automated-review-notes.md` (companion to `docs/audit-community-guidelines.md`). The doc declares the deliberate desktop-only Node usage (temp-file I/O under `os.tmpdir()` with `finally` cleanup, `execFile`-only subprocesses with argument arrays, the `shellEnv()` env allowlist, write-only clipboard), records the wontfix rationale for the node-loader require pattern and the `:has()` toast selectors, rebuts the false positives (no system-identity reads; test-only `globalThis`), and **links to in-code comments rather than duplicating them**. The one genuine fix landed: summarize's deprecated `querySelectorAll` + `Array.from().find()` replaced with Obsidian's typed `containerEl.findAll()` (feature-detected so the vitest environment no-ops cleanly). README now declares clipboard use is write-only and links the doc.
**Alternatives considered**:
- **Fix everything the reviewer flagged** — rejected; most flags mis-model declared, intentional design (desktop-gated Node access, the single sanctioned require site, the `:has()` selector fallback), and "fixing" them would regress real decisions this log records.
- **Answer per-release in the issue thread only** — rejected; the review runs on every release, so the same questions recur — the answers belong in one versioned doc both reviewers and future maintainers can be pointed at.
**Rationale**: With no per-release submission step, the automated review is a permanent gate; a standing triage doc turns each run into a diff against known, justified findings instead of a from-scratch re-derivation.
**Impact**: New `docs/automated-review-notes.md`; `README.md` (clipboard declaration + link); `src/summarize/index.ts` (`findAll`), obsidian mock + a scroll-reveal test. The untyped-analysis reproduction and per-rule enumeration live on the tracking issue (#454).
---
## 2026-07-02: Brand refresh — Iris + Gold; glyphs may carry ONE gold gesture via a theme var
**Context**: The brand kit in `assets/brand/` was replaced (2026 Iris + Gold refresh): body color moves from violet `#8b5cf6` to Iris `#5A3EF0`, volt lime `#CCFF00` is retired, and the "impulse" — the one thing Synapse adds — is now always Gold `#FFD23F`. The in-app glyphs previously had a hard rule: pure `currentColor`, never a baked color (`brand-icons.test.ts` failed on any `#`). The new glyph grammar puts the single gold gesture *inside* the glyphs, which that rule forbade.
@ -36,6 +69,8 @@ Decisions listed in reverse chronological order.
**Impact**: `main.ts`, `shared/update-checker.ts`, `shared/credential-field.ts`, `image/preprocess.ts`, `shared/notifications.ts`, `video/settings-section.ts` (redaction call sites); `title/title-detector.ts` (re-export path only). Defense-in-depth + layering cleanup only — no user-facing behavior change.
**Update (2026-07-03)**: this contract is now lint-enforced — see the 2026-07-03 `synapse/no-unredacted-console` entry (#418).
---
## 2026-06-29: Raw caught errors get the same redaction as strings (`redactError`)

View file

@ -1,9 +1,9 @@
# Project Status
**Last updated**: 2026-07-02
**Version**: 1.0.10
**Branch**: `chore/audit-2026-07-02`
**Health**: Green — `tsc` clean, **1871/1871 tests passing (138 files)**, dependency graph acyclic, no critical/high security findings.
**Last updated**: 2026-07-03
**Version**: 1.0.11
**Branch**: `chore/audit-2026-07-03`
**Health**: Green — `tsc` clean, **1872/1872 tests passing (138 files)**, lint clean, dependency graph acyclic, no critical/high security findings.
> Snapshot only. Decision history lives in `DECISIONS.md`; architecture in `ARCHITECTURE.md`.
@ -52,7 +52,7 @@ Top-level helpers: `onboarding.ts` (first-run welcome, #89), `brand-icons.ts` (S
## Current Focus
- **Codebase audit (2026-07-02)** — regrounded all `AGENTS.md` files (root + per-feature) and these human docs against the live code. Two follow-through changes: (1) the `redactError`/`redactSecrets` scrub now reaches the **remaining** direct error-log call sites — `main.ts` lifecycle paths (settings migration, first-run onboarding, incomplete-checkpoint scan, data-folder migration), the update checker, the credential Test-button chip, the image-downscale fallback (`image/preprocess.ts`), and the clipboard-copy catches (`notifications.ts`, `video/settings-section.ts`) — closing the last raw-error gaps from the 2026-06-29 pass; (2) the `title` module's back-compat re-export of `isUntitled` now goes through the `../shared` barrel instead of the internal `shared/title-detector` file, per the barrel-import rule (the canonical home has been `shared/` since #387). `tsc` clean, 1871 tests green, graph acyclic, no critical/high security findings.
- **Codebase audit (2026-07-03)** — architecture and security passes clean; the Obsidian-compliance pass capitalized three notification strings to match the sentence-case convention; machine docs (`AGENTS.md`) and these human docs regrounded. Chores since the last audit: **release 1.0.11**; the **redaction lint gate** (#418) — a custom type-aware ESLint rule (`synapse/no-unredacted-console`) that makes the "every console sink is redacted" contract regression-proof; the **automated-review triage** (#454) — v1.0.11 store-review findings triaged into a reviewer-facing `docs/automated-review-notes.md`, with one real fix (summarize now uses Obsidian's typed `containerEl.findAll()` instead of deprecated `querySelectorAll`); and a dev-tooling bump (wafflestack 0.6.0 → 0.8.0, agent/skill bundles only — no shipped code).
- **Recent feature work (1.0.7 → 1.0.10)**: idempotency bundle — proposal dedup + `maxProposalsPerNote` (#395), notice throttle (#396), AI cache/coalescing (#397), prompt-injection fence (#398); version-stamped settings migrations (#93); title collision handling (#408, #414); centralized Review-toast gate (#366); combined / note-content summaries (#367); in-app update check (#365) + "What's new" modal (#375); always-on REM semantic matching (#380); elaboration title signal + anti-fabrication guards (#387); unified folder-scan pickers defaulting to vault root (1.0.9); per-section and global reset-to-defaults (1.0.10).
---
@ -62,7 +62,7 @@ Top-level helpers: `onboarding.ts` (first-run welcome, #89), `brand-icons.ts` (S
- The full audit found **no critical or high vulnerabilities**; the codebase is security-mature.
- API keys live in `data.json`, which is **gitignored and never committed** — no secrets in the repo.
- Subprocess calls use `execFile` with argument arrays (no shell); API auth is header-based and HTTPS-only; AI responses are sanitized before being written to notes.
- Secret redaction has a single source of truth (`shared/redact.ts`), used on **every error path** — the AI client, credential validation, and all of `notifications.ts` (error toast, `notifyError`, per-operation `console.error`). `redactError(value)` extends the same scrub to **raw caught errors** (Error `.stack`/`.message`) at **every** direct error console sink: audio, rem, elaboration (×2), and fire-and-forget, plus `main.ts` lifecycle paths (settings migration, first-run onboarding, incomplete-checkpoint scan, data-folder migration), the update checker, the credential Test-button chip, the image-downscale fallback (`image/preprocess.ts`), and the clipboard-copy catches (`notifications.ts`, `video/settings-section.ts`). Covers OpenAI/Anthropic `sk-`, `key-`, Deepgram `dg-`, `Bearer`/`Token`, `anthropic-`, and Google `AIza` keys.
- Secret redaction has a single source of truth (`shared/redact.ts`), used on **every error path** — the AI client, credential validation, and all of `notifications.ts` (error toast, `notifyError`, per-operation `console.error`). `redactError(value)` extends the same scrub to **raw caught errors** (Error `.stack`/`.message`) at **every** direct error console sink: audio, rem, elaboration (×2), and fire-and-forget, plus `main.ts` lifecycle paths (settings migration, first-run onboarding, incomplete-checkpoint scan, data-folder migration), the update checker, the credential Test-button chip, the image-downscale fallback (`image/preprocess.ts`), and the clipboard-copy catches (`notifications.ts`, `video/settings-section.ts`). Covers OpenAI/Anthropic `sk-`, `key-`, Deepgram `dg-`, `Bearer`/`Token`, `anthropic-`, and Google `AIza` keys. The contract is now **lint-enforced** (#418): a custom type-aware ESLint rule (`synapse/no-unredacted-console`) fails CI if any value reaching a `console.*` sink isn't statically string-like (i.e. already scrubbed).
- **Prompt-injection fence** (#398): fetched untrusted content (article/tweet/Reddit bodies, image analysis) is wrapped via `wrapUntrusted` — labeled delimiters + data-not-instructions frame + anti-breakout scrubbing — a structural (not lexical) defense. Gemini audio instructions also go in `system_instruction`.
- Credential validation (#335) probes each provider with one minimal GET; results route through redaction and are **ephemeral** (never persisted).
- Multipart Whisper bodies sanitize vault-derived field/file names (`sanitizeMultipartHeaderValue`).
@ -109,5 +109,6 @@ No npm runtime dependencies.
|---------|---------|
| `npm run dev` | esbuild watch (development) |
| `npm run build` | `tsc -noEmit -skipLibCheck` + esbuild production bundle |
| `npm test` | Vitest — **1871/1871 passing** (138 files) |
| `npm test` | Vitest — **1872/1872 passing** (138 files) |
| `npm run test:coverage` | Vitest with coverage |
| `npm run lint` | ESLint — `obsidianmd/*` store-review mirror + custom `synapse/no-unredacted-console` redaction gate (#418) |