diff --git a/bun.lock b/bun.lock index a7af293fb..da3296c28 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@electric-sql/pglite": "^0.4.4", "@modelcontextprotocol/sdk": "^1.0.0", "gray-matter": "^4.0.3", + "marked": "^18.0.0", "openai": "^4.0.0", "pgvector": "^0.2.0", "postgres": "^3.4.0", @@ -358,6 +359,8 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "marked": ["marked@18.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], diff --git a/package.json b/package.json index 6cc3dc233..532e59068 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@electric-sql/pglite": "^0.4.4", "@modelcontextprotocol/sdk": "^1.0.0", "gray-matter": "^4.0.3", + "marked": "^18.0.0", "openai": "^4.0.0", "pgvector": "^0.2.0", "postgres": "^3.4.0" diff --git a/skills/migrations/v0.9.1.md b/skills/migrations/v0.9.1.md index f0bfa9b1f..32cb7d4d9 100644 --- a/skills/migrations/v0.9.1.md +++ b/skills/migrations/v0.9.1.md @@ -20,6 +20,19 @@ The content hash now includes all page fields (title, type, frontmatter), not just compiled_truth + timeline. This means every existing page's hash will differ on next sync, triggering a full re-import. This is expected and correct. +### Slug authority (BREAKING for some brains) +The file path is now the source of truth for slugs. If a markdown file has a +frontmatter `slug:` field that disagrees with its path, the import is REJECTED +(skipped with an error). This prevents page-hijack attacks in shared brains. + +**If your brain uses frontmatter `slug:` overrides intentionally**, those pages +will be skipped on next sync. Fix by either: +- Removing the `slug:` line from frontmatter (let the path determine the slug) +- Moving the file to match the declared slug + +Run `gbrain sync` and check for "Skipped ... does not match path-derived slug" +errors. Fix any before proceeding. + ### Search limit ceiling Search results are now capped at 100. If your agent requests more, it gets a warning and 100 results. Use the new `--offset` flag to paginate. diff --git a/src/commands/files.ts b/src/commands/files.ts index 354b984bf..643aa2dec 100644 --- a/src/commands/files.ts +++ b/src/commands/files.ts @@ -240,9 +240,10 @@ async function uploadRaw(args: string[]) { uploaded: new Date().toISOString(), ...(fileType ? { type: fileType } : {}), }); - // Write pointer next to the page that references it - pointerPath = `${pageSlug}/${filename}.redirect.yaml`; - console.error(`Pointer: ${pointerPath}`); + // Write pointer next to the original file + pointerPath = filePath + '.redirect.yaml'; + writeFileSync(pointerPath, pointer); + console.error(`Pointer written: ${pointerPath}`); } // Record in DB diff --git a/src/commands/publish.ts b/src/commands/publish.ts index 91e45f28d..54498ab55 100644 --- a/src/commands/publish.ts +++ b/src/commands/publish.ts @@ -14,7 +14,12 @@ import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { randomBytes, createCipheriv, pbkdf2Sync } from 'crypto'; -import { dirname, basename } from 'path'; +import { dirname, basename, join } from 'path'; +import { createRequire } from 'module'; + +// Inline marked.js so published HTML is truly self-contained (no CDN dependency) +const require = createRequire(import.meta.url); +const MARKED_JS = readFileSync(join(dirname(require.resolve('marked')), 'marked.umd.js'), 'utf8'); // ── Content stripping ────────────────────────────────────────────── @@ -314,7 +319,7 @@ export function generateHtml({ title, markdown, encrypted }: GenerateHtmlOptions ${passwordHtml}
${encryptedVars} -