added an api for obsidian integration

This commit is contained in:
venus
2026-09-14 00:47:08 -05:00
parent 9aa0ada81d
commit 3a21998d60
4 changed files with 409 additions and 9 deletions
+7 -2
View File
@@ -6,10 +6,15 @@ import path from "node:path";
const postsDir = path.resolve("src/posts");
test("Content Contract: All posts in src/posts/ satisfy frontmatter schema", () => {
assert.ok(fs.existsSync(postsDir), "src/posts directory must exist");
if (!fs.existsSync(postsDir)) {
fs.mkdirSync(postsDir, { recursive: true });
}
const postFiles = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
assert.ok(postFiles.length > 0, "There must be at least one published post");
if (postFiles.length === 0) {
// When posts are decoupled and mounted externally or ingested via API
return;
}
for (const file of postFiles) {
const filePath = path.join(postsDir, file);
+226
View File
@@ -0,0 +1,226 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import http from "node:http";
import { createServer } from "../server.js";
const TEST_SECRET = "super-secret-test-token";
test("Ingestion API: rejects requests without proper Bearer token", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
// 1. Missing Authorization header
const resNoAuth = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "test", content: "test" })
});
assert.strictEqual(resNoAuth.status, 401, "Missing auth header must return 401");
// 2. Invalid Bearer token
const resBadAuth = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer wrong-token"
},
body: JSON.stringify({ slug: "test", content: "test" })
});
assert.strictEqual(resBadAuth.status, 401, "Invalid token must return 401");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
test("Ingestion API: validates required fields and frontmatter schema", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
// Missing slug
const resNoSlug = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({ content: "---\ntitle: 'Test'\ndate: 2026-09-14\n---\nHello" })
});
assert.strictEqual(resNoSlug.status, 400, "Missing slug must return 400");
// Missing required frontmatter title
const resNoTitle = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "no-title-post",
content: "---\ndate: 2026-09-14\n---\nHello"
})
});
assert.strictEqual(resNoTitle.status, 400, "Missing frontmatter title must return 400");
// Missing required frontmatter date or created
const resNoDate = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "no-date-post",
content: "---\ntitle: 'Valid Title'\n---\nHello"
})
});
assert.strictEqual(resNoDate.status, 400, "Missing frontmatter created/date must return 400");
// Valid with created instead of date
const resWithCreated = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "created-tag-post",
content: "---\ntitle: 'Valid Title'\ncreated: 2026-09-14\n---\nHello"
})
});
assert.strictEqual(resWithCreated.status, 200, "Valid post with created tag must return 200");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
test("Ingestion API: publishes markdown, normalizes wikilinks, saves assets, and handles delete", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
const tmpAssets = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-assets-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts, tmpAssets);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
const rawMarkdown = `---
title: "Reverse Engineering Flock Safety"
created: 2026-09-14
tags: ["#security", "#firmware"]
excerpt: "A deep dive into firmware."
---
Check out [[other-post|Other Post]] and also [[simple-link]].
Here is an image: ![[camera-board.png]]
`;
// 1x1 transparent PNG base64
const sampleBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
try {
// Publish post
const resPublish = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "flock-firmware",
content: rawMarkdown,
assets: [
{
filename: "camera-board.png",
data: sampleBase64
}
]
})
});
assert.strictEqual(resPublish.status, 200, "Valid publish must return 200");
const json = await resPublish.json();
assert.strictEqual(json.status, "published");
assert.strictEqual(json.path, "/posts/flock-firmware/");
// Verify markdown file written and wikilinks normalized
const writtenPath = path.join(tmpPosts, "flock-firmware.md");
assert.ok(fs.existsSync(writtenPath), "Post markdown file must be written to disk");
const writtenContent = fs.readFileSync(writtenPath, "utf-8");
assert.match(writtenContent, /\[Other Post\]\(\/posts\/other-post\/\)/, "Wikilink with alias must be translated");
assert.match(writtenContent, /\[simple-link\]\(\/posts\/simple-link\/\)/, "Simple wikilink must be translated");
assert.match(writtenContent, /!\[camera-board\.png\]\(\/assets\/images\/posts\/camera-board\.png\)/, "Attachment wikilink must be translated");
// Verify asset image was written
const assetPath = path.join(tmpAssets, "camera-board.png");
assert.ok(fs.existsSync(assetPath), "Asset file must be saved");
// Delete post
const resDelete = await fetch(`http://127.0.0.1:${port}/api/posts?slug=flock-firmware`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${TEST_SECRET}`
}
});
assert.strictEqual(resDelete.status, 200, "Delete must return 200");
assert.ok(!fs.existsSync(writtenPath), "Post markdown file must be deleted");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
fs.rmSync(tmpAssets, { recursive: true, force: true });
}
});
test("Ingestion API: adversarial protection against path traversal in slug", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
const resTraversal = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "../../etc/evil",
content: "---\ntitle: 'Evil'\ndate: 2026-09-14\n---\nEvil content"
})
});
// Should sanitize or reject slug containing path traversal
if (resTraversal.status === 200) {
const evilPath = path.resolve(tmpPosts, "../../etc/evil.md");
assert.ok(!fs.existsSync(evilPath), "Must never write outside posts directory");
} else {
assert.strictEqual(resTraversal.status, 400, "Invalid slug must return 400");
}
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
+4 -4
View File
@@ -40,7 +40,7 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains",
// Verify dynamic subdomains
const gitService = site.services.find((s) => s.name === "Gitea");
assert.ok(gitService, "Gitea service exists");
assert.equal(gitService.url, "https://git.testcluster.internal");
assert.equal(gitService.url, "https://git.riverrooks.dev/venus");
assert.equal(gitService.tag, "git.testcluster.internal:2222");
const chefService = site.services.find((s) => s.name === "CyberChef");
@@ -52,9 +52,9 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains",
assert.ok(navHome, "Nav Home link exists");
assert.equal(navHome.url, "/");
const navGit = site.nav.find((n) => n.label === "Gitea");
assert.ok(navGit, "Nav Gitea link exists");
assert.equal(navGit.url, "https://git.testcluster.internal");
const navCode = site.nav.find((n) => n.label === "Code");
assert.ok(navCode, "Nav Code link exists");
assert.equal(navCode.url, "https://git.riverrooks.dev/venus");
});
test("Site Config: respects explicit SITE_URL override", async () => {