ODIAN Launcher: hub, installer, local stack and remembered sign-in (M2)
- Tauri 2 + Svelte 5 hub in ODIAN dark: patch notes and roadmap, install and update progress, local stack status, settings with DHP and MCP addresses. - crates/odian-packs: Rust installer for pack format v1 (signed manifests, resumable verified downloads from a DHP or an ODIAN drive), byte-compatible with ODIAN_MCP_Server's Python odian_packs. - Local stack on 127.0.0.1: Postgres -> Engine API -> tile server -> Odian.exe, one Job Object, clean shutdown; database snapshot restore on a fresh PC only. - Keycloak sign-in through the DHP's MCP, remembered until Sign out with a launcher session that renews the Engine token silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// Patch notes come from the site (or its cached copy) and are untrusted:
|
||||
// Markdown is rendered with marked, then every byte of HTML goes through
|
||||
// DOMPurify with a small allowlist before it reaches {@html}.
|
||||
import { Marked, type Token, type TokensList } from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
const marked = new Marked({ gfm: true, breaks: false, async: false });
|
||||
|
||||
const ALLOWED_TAGS = [
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"p", "br", "hr", "blockquote",
|
||||
"ul", "ol", "li",
|
||||
"strong", "em", "b", "i", "del", "s", "code", "pre", "kbd", "sub", "sup",
|
||||
"a",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"input", // GFM task-list checkboxes (forced disabled below)
|
||||
];
|
||||
|
||||
const ALLOWED_ATTR = ["href", "title", "align", "start", "type", "checked", "disabled"];
|
||||
|
||||
const SAFE_HREF = /^(https?:|mailto:|#)/i;
|
||||
|
||||
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
||||
if (node.tagName === "A") {
|
||||
const href = node.getAttribute("href") ?? "";
|
||||
if (!SAFE_HREF.test(href.trim())) node.removeAttribute("href");
|
||||
node.removeAttribute("target");
|
||||
node.setAttribute("rel", "noopener noreferrer");
|
||||
if (href && !node.getAttribute("title")) node.setAttribute("title", href);
|
||||
}
|
||||
if (node.tagName === "INPUT") {
|
||||
if (node.getAttribute("type") !== "checkbox") node.remove();
|
||||
else node.setAttribute("disabled", "");
|
||||
}
|
||||
});
|
||||
|
||||
function sanitize(html: string): string {
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS,
|
||||
ALLOWED_ATTR,
|
||||
ALLOW_DATA_ATTR: false,
|
||||
ALLOW_ARIA_ATTR: false,
|
||||
});
|
||||
}
|
||||
|
||||
function render(tokens: Token[], links: TokensList["links"]): string {
|
||||
const list = Object.assign(tokens, { links }) as TokensList;
|
||||
return sanitize(marked.parser(list));
|
||||
}
|
||||
|
||||
function isRoadmapHeading(token: Token): boolean {
|
||||
return token.type === "heading" && token.depth === 2 && /^roadmap$/i.test(token.text.trim());
|
||||
}
|
||||
|
||||
export interface RenderedNotes {
|
||||
/** Everything except the Roadmap section. */
|
||||
notes: string;
|
||||
/** Contents under `## Roadmap` (heading excluded), or null when absent/empty. */
|
||||
roadmap: string | null;
|
||||
}
|
||||
|
||||
export function renderNotes(markdown: string): RenderedNotes {
|
||||
const tokens = marked.lexer(markdown);
|
||||
const notes: Token[] = [];
|
||||
const roadmap: Token[] = [];
|
||||
let inRoadmap = false;
|
||||
let sawRoadmap = false;
|
||||
|
||||
for (const token of tokens) {
|
||||
if (isRoadmapHeading(token)) {
|
||||
inRoadmap = true;
|
||||
sawRoadmap = true;
|
||||
continue;
|
||||
}
|
||||
if (inRoadmap && token.type === "heading" && token.depth <= 2) inRoadmap = false;
|
||||
(inRoadmap ? roadmap : notes).push(token);
|
||||
}
|
||||
|
||||
const hasRoadmapContent = roadmap.some((t) => t.type !== "space");
|
||||
return {
|
||||
notes: render(notes, tokens.links),
|
||||
roadmap: sawRoadmap && hasRoadmapContent ? render(roadmap, tokens.links) : null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user