knosky 0.4.0 → 0.4.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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/core/fs-indexer.mjs +26 -8
- package/mcp/server.mjs +1 -1
- package/package.json +1 -1
- package/renderer/city.template.html +14 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to KnoSky. Versions are git-tagged on this repo.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] - 2026-06-29 — Security review fixes
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
- **The fail-closed secret scan now actually fails closed.** It previously ran *after* scrubbing, so a secret in a title/heading/summary was silently redacted instead of blocking the build. It now scans the **un-scrubbed** projections before serialization and blocks (the post-scrub residual is kept as a defense-in-depth gap detector). A secret in a heading now stops the build.
|
|
9
|
+
- **Embedded mode is fail-closed.** The postMessage bridge now requires a non-empty `window.__KC_ALLOWED_ORIGINS` allowlist (the bridge is disabled if it is empty), only accepts messages from the embedding parent frame, and never broadcasts readiness to `"*"`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `test/security-fixtures.mjs` — pure-Node regression tests (secret block, ignore rules, XSS escaping, embed fail-closed). Run `node test/security-fixtures.mjs`.
|
|
13
|
+
- `.github/workflows/ci.yml` — runs the fixtures + `npm audit` on every PR.
|
|
14
|
+
- Root `package-lock.json` for reproducible `npx` / `npm ci` installs.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- README: removed a "zero data risk" line that contradicted PRIVACY.md.
|
|
18
|
+
|
|
5
19
|
## [0.4.0] - 2026-06-29 — File Connections + churn (code-intel)
|
|
6
20
|
|
|
7
21
|
### Added
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Your project grows faster than anyone can hold in their head. Hundreds of files,
|
|
|
21
21
|
- **See your whole project in one screen.** Instead of scrolling a file tree, you see the *shape* of everything — which areas are big, how they connect, where the gaps are. New collaborators get oriented in minutes, not weeks.
|
|
22
22
|
- **Find anything in seconds.** Search the city — or ask your assistant *"where does auth live / what did we decide about billing"* — and jump straight to the **live file**.
|
|
23
23
|
- **Your AI answers from YOUR source, with citations.** Connect it to Claude / Cursor / VS Code / Gemini and your assistant stops guessing about your codebase — it cites the real file, every time.
|
|
24
|
-
- **Zero setup tax,
|
|
24
|
+
- **Zero setup tax, local-first by default.** Point it at a folder → a city in under a minute. Deterministic, **$0 tokens** to keep fresh, **nothing ever leaves your machine**, and a fail-closed secret scan before you share. (KnoSky does not claim "zero data risk" — see [PRIVACY.md](./PRIVACY.md).)
|
|
25
25
|
|
|
26
26
|
**Net:** faster comprehension, reliable recall of your own knowledge, and a grounded AI — without giving up privacy or paying a cent.
|
|
27
27
|
|
package/core/fs-indexer.mjs
CHANGED
|
@@ -143,6 +143,20 @@ if (!NO_CHURN) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// --- fail-closed secret scan on RAW projections, BEFORE scrubbing (the decisive boundary).
|
|
147
|
+
// Scans exactly the text that will be emitted (title/summary/headings/tags + relative path) in
|
|
148
|
+
// its un-scrubbed form, so detection cannot be defeated by serializeNode()'s scrub. ---
|
|
149
|
+
const _NL = String.fromCharCode(10);
|
|
150
|
+
const rawText = raw.map(n => [
|
|
151
|
+
n.title, n.summary,
|
|
152
|
+
(n.headings || []).join(_NL),
|
|
153
|
+
(n.tags || []).join(' '),
|
|
154
|
+
n.provenance && n.provenance.ref,
|
|
155
|
+
].filter(Boolean).join(_NL)).join(_NL);
|
|
156
|
+
const preHits = findSecrets(rawText);
|
|
157
|
+
const preTotal = preHits.reduce((a, h) => a + h[1], 0);
|
|
158
|
+
const preList = preHits.map(h => h[0] + ':' + h[1]).join(', ');
|
|
159
|
+
|
|
146
160
|
const nodes = raw.map(serializeNode);
|
|
147
161
|
const catIds = [...new Set(nodes.map(n => n.category))].sort();
|
|
148
162
|
const categories = deriveCategories(catIds);
|
|
@@ -170,21 +184,25 @@ console.log('post-scrub leak hits (emails/keys, should be ~0):', leakHits);
|
|
|
170
184
|
console.log('redacted-path files skipped:', redactedPaths);
|
|
171
185
|
if (flags.length) console.log('flags:', flags.join('; '));
|
|
172
186
|
|
|
173
|
-
// --- fail-closed safe-share audit
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
187
|
+
// --- fail-closed safe-share audit.
|
|
188
|
+
// PRIMARY = pre-scrub detection (preTotal, computed above) -> the real boundary.
|
|
189
|
+
// SECONDARY = post-scrub residual on the emitted artifact -> flags scrub gaps (defense in depth). ---
|
|
190
|
+
const residualHits = findSecrets(blob);
|
|
191
|
+
const residualTotal = residualHits.reduce((a, h) => a + h[1], 0);
|
|
192
|
+
const residualList = residualHits.map(h => h[0] + ':' + h[1]).join(', ');
|
|
177
193
|
if (SHARE_SAFE) {
|
|
178
194
|
console.log('');
|
|
179
195
|
console.log('KnoSky safety report');
|
|
180
196
|
console.log('- Files scanned:', scanned);
|
|
181
197
|
console.log('- Files skipped (redact-path):', redactedPaths);
|
|
182
|
-
console.log('- Secrets found:',
|
|
198
|
+
console.log('- Secrets found (pre-scrub):', preTotal, preTotal ? '[' + preList + ']' : '');
|
|
199
|
+
console.log('- Residual after scrub (should be 0):', residualTotal, residualTotal ? '[' + residualList + ']' : '');
|
|
183
200
|
console.log('- Absolute path in output:', INCLUDE_ABS ? 'INCLUDED' : 'removed (basename only)');
|
|
184
|
-
console.log('- Risk level:',
|
|
201
|
+
console.log('- Risk level:', (preTotal || residualTotal) ? 'HIGH' : 'Low');
|
|
185
202
|
}
|
|
186
|
-
if (
|
|
187
|
-
|
|
203
|
+
if ((preTotal || residualTotal) && !ALLOW_LEAKS) {
|
|
204
|
+
const why = preTotal ? preList : residualList;
|
|
205
|
+
console.error('BLOCKED: ' + (preTotal || residualTotal) + ' secret-like value(s) detected [' + why + ']. Nothing written.');
|
|
188
206
|
console.error('Add the offending paths to .kcignore or pass --redact <term>, then rebuild. Override with --allow-leaks (not recommended).');
|
|
189
207
|
process.exit(1);
|
|
190
208
|
}
|
package/mcp/server.mjs
CHANGED
|
@@ -9,7 +9,7 @@ const CITY = process.env.KC_CITY || process.argv[2];
|
|
|
9
9
|
if (!CITY) { console.error("Knowledge City MCP: set KC_CITY (or pass a path) to a city-data.v2.json"); process.exit(1); }
|
|
10
10
|
const ctx = load(CITY);
|
|
11
11
|
|
|
12
|
-
const server = new McpServer({ name: "knowledge-city", version: "0.4.
|
|
12
|
+
const server = new McpServer({ name: "knowledge-city", version: "0.4.1" });
|
|
13
13
|
|
|
14
14
|
server.registerTool("kc_search", {
|
|
15
15
|
title: "Search the Knowledge City",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knosky",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Turn any repo or docs folder into an explorable city, plus a local MCP that grounds your AI in your own source with citations. Local-first, free, $0 tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -690,20 +690,25 @@
|
|
|
690
690
|
|
|
691
691
|
// ---------- host bridge over postMessage (embedded mode ONLY; OFF in the standalone gift) ----------
|
|
692
692
|
if(window.__KC_EMBED__){
|
|
693
|
-
var KC_OK=window.__KC_ALLOWED_ORIGINS
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
693
|
+
var KC_OK=(Array.isArray(window.__KC_ALLOWED_ORIGINS)?window.__KC_ALLOWED_ORIGINS:[]).filter(Boolean);
|
|
694
|
+
if(!KC_OK.length){
|
|
695
|
+
console.warn('KnoSky embed disabled: window.__KC_ALLOWED_ORIGINS is empty. Set at least one allowed origin to enable the postMessage bridge.');
|
|
696
|
+
}else{
|
|
697
|
+
window.addEventListener('message',function(ev){
|
|
698
|
+
if(KC_OK.indexOf(ev.origin)===-1)return; // origin allowlist (REQUIRED, fail-closed)
|
|
699
|
+
if(ev.source!==window.parent)return; // only accept from the embedding parent
|
|
700
|
+
var d=ev&&ev.data;if(!d||typeof d!=='object')return;
|
|
701
|
+
if(d.type==='kc:data'){ingest(d.payload);return;}
|
|
702
|
+
if(d.type==='kc:refit'){resize();if(typeof fitAll==='function'){fitAll();cam.x=cam.tx;cam.y=cam.ty;cam.zoom=cam.tz;}return;}
|
|
703
|
+
if(d.type==='kc:focus'&&d.id!=null){var id=String(d.id);if(byId[id]){selectBuilding(id);}}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
701
706
|
}
|
|
702
707
|
if(window.__KC_KENNEY__)loadKenney();
|
|
703
708
|
// Standalone (local product): render immediately from injected page data.
|
|
704
709
|
try{ if(window.__KC_DATA__&&typeof window.__KC_DATA__==='object'&&Array.isArray(window.__KC_DATA__.nodes)) ingest(window.__KC_DATA__); }catch(e){}
|
|
705
710
|
// Announce readiness to an authenticated parent (embedded mode only, no wildcard broadcast in standalone).
|
|
706
|
-
if(window.__KC_EMBED__){try{window.parent&&window.parent.postMessage({type:"kc:ready"},
|
|
711
|
+
if(window.__KC_EMBED__){var KC_RDY=(Array.isArray(window.__KC_ALLOWED_ORIGINS)?window.__KC_ALLOWED_ORIGINS:[]).filter(Boolean);if(KC_RDY.length){try{window.parent&&window.parent.postMessage({type:"kc:ready"},KC_RDY[0]);}catch(e){}}}
|
|
707
712
|
})();
|
|
708
713
|
</script>
|
|
709
714
|
</body>
|