img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+
+export {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/packages/mtm-admin/src/components/ui/input.tsx b/packages/mtm-admin/src/components/ui/input.tsx
new file mode 100644
index 0000000..6a9e6d0
--- /dev/null
+++ b/packages/mtm-admin/src/components/ui/input.tsx
@@ -0,0 +1,20 @@
+import * as React from "react"
+import { Input as InputPrimitive } from "@base-ui/react/input"
+
+import { cn } from "../../lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/packages/mtm-admin/src/components/ui/label.tsx b/packages/mtm-admin/src/components/ui/label.tsx
new file mode 100644
index 0000000..4cdc027
--- /dev/null
+++ b/packages/mtm-admin/src/components/ui/label.tsx
@@ -0,0 +1,20 @@
+"use client"
+
+import * as React from "react"
+
+import { cn } from "../../lib/utils"
+
+function Label({ className, ...props }: React.ComponentProps<"label">) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/packages/mtm-admin/src/components/ui/switch.tsx b/packages/mtm-admin/src/components/ui/switch.tsx
new file mode 100644
index 0000000..7e62120
--- /dev/null
+++ b/packages/mtm-admin/src/components/ui/switch.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
+
+import { cn } from "../../lib/utils"
+
+function Switch({
+ className,
+ size = "default",
+ ...props
+}: SwitchPrimitive.Root.Props & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/packages/mtm-admin/src/components/ui/textarea.tsx b/packages/mtm-admin/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..068f0b4
--- /dev/null
+++ b/packages/mtm-admin/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "../../lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/packages/mtm-admin/src/config.test.ts b/packages/mtm-admin/src/config.test.ts
new file mode 100644
index 0000000..e03ccab
--- /dev/null
+++ b/packages/mtm-admin/src/config.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it } from "vitest";
+import { validateAdminOAuthConfig, type AdminOAuthConfig } from "./config";
+
+const valid: AdminOAuthConfig = {
+ issuer: "https://auth.example.test",
+ clientId: "mtm-admin-web-v1",
+ redirectUri: "https://admin.example.test/",
+ resource: "https://auth.example.test/api/system",
+ scopes: ["openid", "gomtm:admin"],
+};
+
+describe("Admin OAuth config", () => {
+ it("requires the dedicated control-plane resource and scope", () => {
+ expect(() => validateAdminOAuthConfig(valid)).not.toThrow();
+ expect(() => validateAdminOAuthConfig({ ...valid, resource: "https://auth.example.test/api/dsh" })).toThrow("control plane");
+ expect(() => validateAdminOAuthConfig({ ...valid, scopes: ["openid"] })).toThrow("gomtm:admin");
+ });
+
+ it("requires a canonical HTTPS issuer", () => {
+ expect(() => validateAdminOAuthConfig({ ...valid, issuer: "http://auth.example.test" })).toThrow("HTTPS origin");
+ expect(() => validateAdminOAuthConfig({ ...valid, issuer: "https://auth.example.test/path", resource: "https://auth.example.test/path/api/system" })).toThrow("HTTPS origin");
+ });
+});
diff --git a/packages/mtm-admin/src/config.ts b/packages/mtm-admin/src/config.ts
new file mode 100644
index 0000000..6051306
--- /dev/null
+++ b/packages/mtm-admin/src/config.ts
@@ -0,0 +1,70 @@
+export interface AdminOAuthConfig {
+ issuer: string;
+ clientId: string;
+ redirectUri: string;
+ resource: string;
+ discoveryUrl?: string;
+ scopes: readonly string[];
+}
+
+export type AdminAuthStatus = "signed-out" | "discovering" | "ready" | "authorizing" | "authenticated" | "error";
+
+export interface AdminAuthSnapshot {
+ status: AdminAuthStatus;
+ error?: string;
+}
+
+export interface AdminAuthClient {
+ getAccessToken(): Promise
;
+ getSnapshot(): AdminAuthSnapshot;
+ subscribe(listener: () => void): () => void;
+ clear(): void;
+ beginLogin(options?: { selectAccount?: boolean }): Promise;
+ consumeCallback(callbackUrl?: string): Promise;
+ logout(): Promise;
+ dispose(options?: { preserveAuthorization?: boolean }): void;
+}
+
+export interface AdminAppOptions {
+ apiOrigin: string;
+ oauth: AdminOAuthConfig;
+ /** Explicit programmatic auth adapter for tests or a trusted host. */
+ auth?: AdminAuthClient;
+}
+
+export type AdminBootstrapConfig = Omit;
+
+declare global {
+ interface Window {
+ __MTM_ADMIN_CONFIG__?: AdminBootstrapConfig;
+ }
+}
+
+export function validateAdminOAuthConfig(config: AdminOAuthConfig): void {
+ let issuer: URL;
+ try {
+ issuer = new URL(config.issuer);
+ } catch {
+ throw new TypeError("Admin OAuth issuer must be an absolute HTTPS origin");
+ }
+ if (issuer.protocol !== "https:" || issuer.username || issuer.password || issuer.pathname !== "/" || issuer.search || issuer.hash) {
+ throw new TypeError("Admin OAuth issuer must be an absolute HTTPS origin");
+ }
+ if (config.resource !== issuer.origin + "/api/system") throw new TypeError("Admin OAuth resource must be the gomtm control plane");
+ if (!config.scopes.includes("openid") || !config.scopes.includes("gomtm:admin")) {
+ throw new TypeError("Admin OAuth scopes must include openid and gomtm:admin");
+ }
+}
+
+export function normalizeAdminOrigin(value: string): string {
+ let url: URL;
+ try {
+ url = new URL(value.trim());
+ } catch {
+ throw new TypeError("apiOrigin must be an absolute URL");
+ }
+ if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
+ throw new TypeError("apiOrigin must be an origin URL");
+ }
+ return url.origin;
+}
diff --git a/packages/mtm-admin/src/embed.tsx b/packages/mtm-admin/src/embed.tsx
new file mode 100644
index 0000000..0d29224
--- /dev/null
+++ b/packages/mtm-admin/src/embed.tsx
@@ -0,0 +1,15 @@
+import { createRoot, type Root } from "react-dom/client";
+import { AdminApp, type AdminAppOptions } from "./index";
+import "./styles.css";
+
+const roots = new WeakMap();
+
+export function mount(element: Element, options: AdminAppOptions): () => void {
+ const root = roots.get(element) ?? createRoot(element);
+ roots.set(element, root);
+ root.render();
+ return () => {
+ root.unmount();
+ roots.delete(element);
+ };
+}
diff --git a/packages/mtm-admin/src/i18n.ts b/packages/mtm-admin/src/i18n.ts
new file mode 100644
index 0000000..3e07e42
--- /dev/null
+++ b/packages/mtm-admin/src/i18n.ts
@@ -0,0 +1,13 @@
+import { useMemo } from "react";
+interface TranslationMap { [key: string]: string | TranslationMap; }
+type TranslationValue = string | TranslationMap;
+type Messages = TranslationMap;
+const messages: Record<"en" | "zh", Messages> = {
+ en: { admin: { auth: { title: "MTM Administrator", description: "Authenticate with the gomtm control plane.", authenticationRequired: "Administrator authentication is required.", authenticationFailed: "Administrator authentication failed.", signIn: "Sign in", signingIn: "Signing in..." }, controlPlane: { loading: "Loading admin console", product: "gomtmui control plane", title: "Administrator", signOut: "Sign out", authenticationTitle: "Authentication", authenticationDescription: "Manage registration and identity providers for this gomtmui authority.", memberSignup: "Member registration", memberSignupDescription: "Allow new email/password accounts after root setup.", emailVerification: "Email verification", emailVerificationRequired: "Require verified email before sign-in.", emailVerificationUnavailable: "Unavailable until email delivery is configured.", githubProvider: "GitHub provider", githubProviderDescription: "Enable GitHub only after its credentials are configured.", githubClientId: "GitHub client ID", githubClientSecret: "GitHub client secret", githubClientSecretConfigured: "Configured; leave blank to keep", githubClientSecretPlaceholder: "Enter client secret", saving: "Saving...", saveChanges: "Save changes", authPolicySaved: "Authentication policy saved", errors: { adminConsoleUnavailable: "Admin console unavailable", authConfigUnavailable: "Authentication configuration is unavailable", authConfigUpdateFailed: "Authentication configuration update failed", authConfigInvalid: "Authentication configuration is invalid", emailDeliveryUnavailable: "Email verification requires configured email delivery", authRequired: "Authentication is required", authUnavailable: "Authentication is unavailable", platformAdminRequired: "Platform admin access is required" } }, p2p: { title: "P2P bootstrap host", description: "Expose the libp2p WSS host used by gomtm server --bootstrap.", loading: "Loading P2P host", refresh: "Refresh status", copyAddress: "Copy bootstrap address", addressCopied: "Bootstrap address copied", peerId: "Peer ID", bootstrapAddress: "Bootstrap multiaddr", revision: "Revision", generation: "Generation {value}", connections: "Connections", capabilities: "Capabilities (one per line)", services: "Services (one per line)", data: "Snapshot data JSON", save: "Save snapshot", saving: "Saving...", saved: "Snapshot saved", errors: { loadFailed: "Unable to load the P2P bootstrap host.", saveFailed: "Unable to save the snapshot.", invalidData: "Data must be a JSON object with string values.", copyFailed: "Unable to copy the bootstrap address." } }, systemConfig: { title: "System configuration", description: "Manage the published document consumed by gomtm server runtimes.", export: "Export", import: "Import", publish: "Publish", publishing: "Publishing...", loading: "Loading system configuration", currentRevision: "Current revision:", notPublished: "No configuration has been published yet.", jsonLabel: "System configuration JSON", published: "Published {revision}", importedPublished: "Imported and published {revision}", errors: { loadFailed: "Failed to load system configuration", requestFailed: "System configuration request failed", invalidJson: "Configuration JSON is invalid", publishFailed: "System configuration publish failed", exportFailed: "System configuration export failed", importFailed: "System configuration import failed", unavailable: "System configuration is unavailable", invalid: "System configuration document is invalid", notPublished: "System configuration is not published", importInvalid: "System configuration export is invalid", revisionNotFound: "System configuration revision not found", authRequired: "Authentication is required", authUnavailable: "Authentication is unavailable", platformAdminRequired: "Platform admin access is required", scopeRequired: "The gomtm:admin scope is required" } } } },
+ zh: { admin: { auth: { title: "MTM 管理员", description: "通过 gomtm 控制面完成认证。", authenticationRequired: "需要管理员认证。", authenticationFailed: "管理员认证失败。", signIn: "登录", signingIn: "登录中..." }, controlPlane: { loading: "正在加载管理员控制台", product: "gomtmui 控制面", title: "管理员", signOut: "退出登录", authenticationTitle: "认证", authenticationDescription: "管理此 gomtmui authority 的注册策略和身份提供商。", memberSignup: "成员注册", memberSignupDescription: "根用户完成初始化后允许新的邮箱/密码账号注册。", emailVerification: "邮箱验证", emailVerificationRequired: "登录前必须验证邮箱。", emailVerificationUnavailable: "配置邮件投递后才能启用。", githubProvider: "GitHub 提供商", githubProviderDescription: "配置凭据后才能启用 GitHub。", githubClientId: "GitHub 客户端 ID", githubClientSecret: "GitHub 客户端密钥", githubClientSecretConfigured: "已配置;留空以保留当前值", githubClientSecretPlaceholder: "输入客户端密钥", saving: "保存中...", saveChanges: "保存更改", authPolicySaved: "认证策略已保存", errors: { adminConsoleUnavailable: "管理员控制台不可用", authConfigUnavailable: "认证配置不可用", authConfigUpdateFailed: "认证配置更新失败", authConfigInvalid: "认证配置无效", emailDeliveryUnavailable: "邮箱验证需要先配置邮件投递", authRequired: "需要认证", authUnavailable: "认证服务不可用", platformAdminRequired: "需要平台管理员权限" } }, p2p: { title: "P2P 引导节点", description: "提供 gomtm server --bootstrap 使用的 libp2p WSS 节点。", loading: "正在加载 P2P 节点", refresh: "刷新状态", copyAddress: "复制引导地址", addressCopied: "已复制引导地址", peerId: "Peer ID", bootstrapAddress: "引导 multiaddr", revision: "版本", generation: "代数 {value}", connections: "连接数", capabilities: "能力(每行一项)", services: "服务(每行一项)", data: "Snapshot 数据 JSON", save: "保存 snapshot", saving: "保存中...", saved: "Snapshot 已保存", errors: { loadFailed: "无法加载 P2P 引导节点。", saveFailed: "无法保存 snapshot。", invalidData: "数据必须是值为字符串的 JSON 对象。", copyFailed: "无法复制引导地址。" } }, systemConfig: { title: "系统配置", description: "管理 gomtm 服务运行时使用的已发布配置文档。", export: "导出", import: "导入", publish: "发布", publishing: "发布中...", loading: "正在加载系统配置", currentRevision: "当前版本:", notPublished: "尚未发布配置。", jsonLabel: "系统配置 JSON", published: "已发布 {revision}", importedPublished: "已导入并发布 {revision}", errors: { loadFailed: "加载系统配置失败", requestFailed: "系统配置请求失败", invalidJson: "配置 JSON 无效", publishFailed: "发布系统配置失败", exportFailed: "导出系统配置失败", importFailed: "导入系统配置失败", unavailable: "系统配置不可用", invalid: "系统配置文档无效", notPublished: "系统配置尚未发布", importInvalid: "系统配置导出内容无效", revisionNotFound: "未找到系统配置版本", authRequired: "需要认证", authUnavailable: "认证服务不可用", platformAdminRequired: "需要平台管理员权限", scopeRequired: "需要 gomtm:admin scope" } } } },
+};
+function resolveMessage(source: Messages, path: string): string { const value = path.split(".").reduce((current, segment) => current && typeof current === "object" ? current[segment] : undefined, source); return typeof value === "string" ? value : path; }
+export function useTranslations(namespace: string) {
+ const locale = typeof document !== "undefined" && document.documentElement.lang.startsWith("zh") ? "zh" : typeof navigator !== "undefined" && navigator.language.startsWith("zh") ? "zh" : "en";
+ return useMemo(() => (key: string, values?: Record) => { let value = resolveMessage(messages[locale], namespace + "." + key); for (const [name, replacement] of Object.entries(values ?? {})) value = value.replaceAll("{" + name + "}", String(replacement)); return value; }, [locale, namespace]);
+}
diff --git a/packages/mtm-admin/src/index.tsx b/packages/mtm-admin/src/index.tsx
new file mode 100644
index 0000000..238feba
--- /dev/null
+++ b/packages/mtm-admin/src/index.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { OAuthClient } from "mtmharness/auth";
+import { useEffect, useRef } from "react";
+import { AdminAuthGate } from "./admin-auth";
+import { clearAdminApp, configureAdminApp } from "./admin-fetch";
+import { validateAdminOAuthConfig, type AdminAppOptions, type AdminAuthClient } from "./config";
+
+export function AdminApp(options: AdminAppOptions) {
+ validateAdminOAuthConfig(options.oauth);
+ const authRef = useRef(undefined);
+ const ownsAuthRef = useRef(false);
+ if (authRef.current === undefined) {
+ authRef.current = options.auth ?? new OAuthClient(options.oauth);
+ ownsAuthRef.current = options.auth === undefined;
+ }
+ const auth = authRef.current;
+ if (auth === undefined) throw new Error("mtm-admin auth client is unavailable");
+ configureAdminApp({ apiOrigin: options.apiOrigin, auth });
+
+ useEffect(() => {
+ return () => {
+ clearAdminApp(auth);
+ if (ownsAuthRef.current) auth.dispose({ preserveAuthorization: false });
+ };
+ }, [auth, options.apiOrigin]);
+
+ return ;
+}
+
+export type { AdminAppOptions, AdminAuthClient, AdminOAuthConfig } from "./config";
diff --git a/packages/mtm-admin/src/launcher.test.ts b/packages/mtm-admin/src/launcher.test.ts
new file mode 100644
index 0000000..f608427
--- /dev/null
+++ b/packages/mtm-admin/src/launcher.test.ts
@@ -0,0 +1,50 @@
+// @vitest-environment jsdom
+
+import { afterEach, describe, expect, it } from "vitest";
+import { mount, type MtmharnessFrontendExtensionContext } from "./launcher";
+
+const documentBody = document.body;
+
+afterEach(() => {
+ documentBody.replaceChildren();
+});
+
+function context(): MtmharnessFrontendExtensionContext {
+ const root = document.createElement("div");
+ documentBody.append(root);
+ return {
+ apiVersion: 1,
+ id: "mtm-admin",
+ version: "0.1.0",
+ root,
+ document,
+ signal: new AbortController().signal,
+ registerCleanup: () => undefined,
+ };
+}
+
+describe("mtm-admin launcher", () => {
+ it("opens the versioned standalone app without receiving auth state", () => {
+ const current = context();
+ mount(current);
+ const link = current.root.querySelector("a");
+
+ expect(link?.href).toBe("https://unpkg.com/mtm-admin@0.1.0/dist/standalone/index.html");
+ expect(link?.target).toBe("_blank");
+ expect(link?.rel).toBe("noopener noreferrer");
+ expect(link?.textContent).toBe("Open MTM Admin");
+ });
+
+ it("restores the owned root on cleanup", () => {
+ const current = context();
+ const beforeStyle = current.root.getAttribute("style");
+ const beforeHidden = current.root.hidden;
+ const cleanup = mount(current);
+
+ cleanup();
+
+ expect(current.root.childElementCount).toBe(0);
+ expect(current.root.getAttribute("style")).toBe(beforeStyle);
+ expect(current.root.hidden).toBe(beforeHidden);
+ });
+});
diff --git a/packages/mtm-admin/src/launcher.ts b/packages/mtm-admin/src/launcher.ts
new file mode 100644
index 0000000..91d8f79
--- /dev/null
+++ b/packages/mtm-admin/src/launcher.ts
@@ -0,0 +1,53 @@
+const ADMIN_APP_URL = "https://unpkg.com/mtm-admin@0.1.0/dist/standalone/index.html";
+
+export interface MtmharnessFrontendExtensionContext {
+ readonly apiVersion: 1;
+ readonly id: string;
+ readonly version: string;
+ readonly root: HTMLElement;
+ readonly document: Document;
+ readonly signal: AbortSignal;
+ readonly registerCleanup: (cleanup: () => void | Promise) => void;
+}
+
+/** Mount a token-free entry point for the independent Admin application. */
+export function mount(context: MtmharnessFrontendExtensionContext): () => void {
+ const root = context.root;
+ const previousStyle = root.getAttribute("style");
+ const previousHidden = root.hidden;
+ const link = context.document.createElement("a");
+ let disposed = false;
+ const dispose = (): void => {
+ if (disposed) return;
+ disposed = true;
+ context.signal.removeEventListener("abort", dispose);
+ link.remove();
+ root.hidden = previousHidden;
+ if (previousStyle === null) root.removeAttribute("style");
+ else root.setAttribute("style", previousStyle);
+ };
+
+ link.href = ADMIN_APP_URL;
+ link.target = "_blank";
+ link.rel = "noopener noreferrer";
+ link.textContent = "Open MTM Admin";
+ link.setAttribute("aria-label", "Open MTM Admin");
+ link.dataset.mtmAdminLauncher = "true";
+ link.style.display = "inline-flex";
+ link.style.alignItems = "center";
+ link.style.border = "1px solid #cbd5e1";
+ link.style.borderRadius = "6px";
+ link.style.background = "#ffffff";
+ link.style.color = "#0f172a";
+ link.style.padding = "8px 12px";
+ link.style.font = "600 14px system-ui, sans-serif";
+ link.style.textDecoration = "none";
+ root.style.position = "fixed";
+ root.style.right = "16px";
+ root.style.bottom = "16px";
+ root.style.zIndex = "2147483000";
+ root.append(link);
+ context.signal.addEventListener("abort", dispose, { once: true });
+ context.registerCleanup(dispose);
+ return dispose;
+}
diff --git a/packages/mtm-admin/src/lib/i18n/api-error.ts b/packages/mtm-admin/src/lib/i18n/api-error.ts
new file mode 100644
index 0000000..d67e948
--- /dev/null
+++ b/packages/mtm-admin/src/lib/i18n/api-error.ts
@@ -0,0 +1,17 @@
+export type ApiErrorBody = { error?: { code?: unknown; message?: unknown } };
+
+export function translateApiError(
+ body: unknown,
+ fallback: string,
+ errorKeys: Record,
+ translate: (key: MessageKey) => string,
+): string {
+ const payload = body as ApiErrorBody;
+ const code = typeof payload.error?.code === "string" ? payload.error.code : undefined;
+ const messageKey = code === undefined ? undefined : errorKeys[code];
+ return messageKey === undefined
+ ? typeof payload.error?.message === "string"
+ ? payload.error.message
+ : fallback
+ : translate(messageKey);
+}
diff --git a/packages/mtm-admin/src/lib/utils.ts b/packages/mtm-admin/src/lib/utils.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/packages/mtm-admin/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/packages/mtm-admin/src/main.tsx b/packages/mtm-admin/src/main.tsx
new file mode 100644
index 0000000..a7a567b
--- /dev/null
+++ b/packages/mtm-admin/src/main.tsx
@@ -0,0 +1,14 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { AdminApp } from "./index";
+import "./styles.css";
+
+const root = document.getElementById("root");
+if (!root) throw new Error("mtm-admin root is missing");
+
+const config = window.__MTM_ADMIN_CONFIG__;
+if (!config) {
+ root.textContent = "mtm-admin configuration is missing";
+} else {
+ createRoot(root).render();
+}
diff --git a/packages/mtm-admin/src/package-contract.test.ts b/packages/mtm-admin/src/package-contract.test.ts
new file mode 100644
index 0000000..82e9fac
--- /dev/null
+++ b/packages/mtm-admin/src/package-contract.test.ts
@@ -0,0 +1,18 @@
+import { existsSync, readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { describe, expect, it } from "vitest";
+
+const packageRoot = resolve(import.meta.dirname, "..");
+
+describe("mtm-admin package contract", () => {
+ it("publishes a secondary launcher and independent app entry", () => {
+ const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")) as {
+ exports?: Record;
+ mtmharness?: { secondary?: { id?: string; apiVersion?: number; client?: string } };
+ };
+ expect(manifest.mtmharness?.secondary).toEqual({ id: "mtm-admin", apiVersion: 1, client: "./lib/client.js" });
+ expect(manifest.exports?.["./client"]).toMatchObject({ import: "./lib/client.js" });
+ expect(manifest.exports?.["./app"]).toBe("./dist/standalone/index.html");
+ expect(existsSync(resolve(packageRoot, "public/config.js"))).toBe(true);
+ });
+});
diff --git a/packages/mtm-admin/src/styles.css b/packages/mtm-admin/src/styles.css
new file mode 100644
index 0000000..ed0c0b1
--- /dev/null
+++ b/packages/mtm-admin/src/styles.css
@@ -0,0 +1,159 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+/* @custom-variant dark (&:is(.dark *)); */
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --font-heading: var(--font-sans);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
+}
+
+:root {
+ --radius: 0.625rem;
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+
+ /* Diff & Fonts */
+ --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --font-family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --font-size-small: 0.875rem;
+
+ --syntax-diff-add: oklch(0.7 0.14 150);
+ --syntax-diff-delete: oklch(0.7 0.14 25);
+ --syntax-diff-unknown: oklch(0.7 0.14 80);
+
+ --surface-warning-base: oklch(0.9 0.1 85);
+ --surface-warning-strong: oklch(0.7 0.15 85);
+ --border-warning-base: oklch(0.8 0.15 85);
+ --text-on-warning-strong: oklch(0.2 0.05 85);
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+
+ /* Diff (Dark) */
+ --syntax-diff-add: oklch(0.6 0.14 150);
+ --syntax-diff-delete: oklch(0.6 0.14 25);
+ --syntax-diff-unknown: oklch(0.6 0.14 80);
+
+ --surface-warning-base: oklch(0.3 0.1 85);
+ --surface-warning-strong: oklch(0.5 0.15 85);
+ --border-warning-base: oklch(0.4 0.15 85);
+ --text-on-warning-strong: oklch(0.95 0.05 85);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
+ }
+}
diff --git a/packages/mtm-admin/tsconfig.json b/packages/mtm-admin/tsconfig.json
new file mode 100644
index 0000000..956b828
--- /dev/null
+++ b/packages/mtm-admin/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "declaration": true,
+ "declarationMap": false,
+ "emitDeclarationOnly": true,
+ "jsx": "react-jsx",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "outDir": "lib/types",
+ "rootDir": "src",
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2021",
+ "types": ["vite/client"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
+ "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
+}
diff --git a/packages/mtm-admin/vite.config.ts b/packages/mtm-admin/vite.config.ts
new file mode 100644
index 0000000..3ca2165
--- /dev/null
+++ b/packages/mtm-admin/vite.config.ts
@@ -0,0 +1,16 @@
+import { fileURLToPath } from "node:url";
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ root: fileURLToPath(new URL(".", import.meta.url)),
+ base: "./",
+ plugins: [react(), tailwindcss()],
+ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
+ build: {
+ outDir: fileURLToPath(new URL("./dist/standalone", import.meta.url)),
+ emptyOutDir: true,
+ sourcemap: true,
+ },
+});
diff --git a/packages/mtm-admin/vite.embed.config.ts b/packages/mtm-admin/vite.embed.config.ts
new file mode 100644
index 0000000..7b3ab87
--- /dev/null
+++ b/packages/mtm-admin/vite.embed.config.ts
@@ -0,0 +1,24 @@
+import { fileURLToPath } from "node:url";
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ root: fileURLToPath(new URL(".", import.meta.url)),
+ base: "./",
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
+ plugins: [react(), tailwindcss()],
+ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
+ build: {
+ outDir: fileURLToPath(new URL("./dist/embed", import.meta.url)),
+ emptyOutDir: true,
+ sourcemap: true,
+ cssCodeSplit: false,
+ lib: {
+ entry: fileURLToPath(new URL("./src/embed.tsx", import.meta.url)),
+ name: "MtmAdmin",
+ formats: ["es", "iife"],
+ fileName: (format) => format === "iife" ? "mtm-admin.iife.js" : "mtm-admin.js",
+ },
+ },
+});
diff --git a/packages/mtm-admin/vitest.config.ts b/packages/mtm-admin/vitest.config.ts
new file mode 100644
index 0000000..0c44df4
--- /dev/null
+++ b/packages/mtm-admin/vitest.config.ts
@@ -0,0 +1,15 @@
+import { fileURLToPath } from "node:url";
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+const packageRoot = fileURLToPath(new URL(".", import.meta.url));
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
+ test: {
+ environment: "jsdom",
+ include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
+ root: packageRoot,
+ },
+});
diff --git a/packages/mtmharness/README.md b/packages/mtmharness/README.md
index ac2e9bd..347a26a 100644
--- a/packages/mtmharness/README.md
+++ b/packages/mtmharness/README.md
@@ -89,7 +89,7 @@ Embed uses memory history and never changes the host page URL. It mounts inside
## Authentication
-The independent client performs discovery-first OAuth/OIDC Authorization Code + PKCE (S256). The full issuer, client ID, exact redirect URI, independent resource, caller-provided scopes, HTTPS endpoints, and provider capabilities are validated before authorization. `openid` is required for ID-token verification; API and refresh scopes come from the registered authority profile. Dynamic client registration is not implemented; production clients and redirect URIs must be registered by the provider.
+The package exposes the reusable browser OAuth client through `mtmharness/auth`; it is the same discovery-first implementation used by the independent client. The independent client performs discovery-first OAuth/OIDC Authorization Code + PKCE (S256). The full issuer, client ID, exact redirect URI, independent resource, caller-provided scopes, HTTPS endpoints, and provider capabilities are validated before authorization. `openid` is required for ID-token verification; API and refresh scopes come from the registered authority profile. Dynamic client registration is not implemented; production clients and redirect URIs must be registered by the provider.
Access and refresh tokens live only in the JavaScript memory of the auth client. A short-lived PKCE transaction containing state/verifier/nonce is the only auth state written to partitioned `sessionStorage`, and it is removed on every callback path. Callback URLs are sanitized after consumption. Tokens, tickets, roles, and capabilities are never put in markup, localStorage, iframe messages, logs, or WebSocket URLs.
@@ -97,6 +97,10 @@ HTTP resource calls, revocation, and `POST /api/dsh/ws-ticket` use an explicit `
The official DSH plugin keeps the host FullShell and local session untouched.
+## Admin launcher
+
+The optional mtm-admin setting loads a pinned, token-free secondary launcher. It opens the independently deployed mtm-admin static application; the Admin OAuth client and bearer token stay in that top-level app. The secondary artifact is released before the mtmharness manifest is updated with its exact version and SHA-256 integrity.
+
## Development
pnpm install
diff --git a/packages/mtmharness/package.json b/packages/mtmharness/package.json
index cdf4ba0..a938a5c 100644
--- a/packages/mtmharness/package.json
+++ b/packages/mtmharness/package.json
@@ -1,6 +1,6 @@
{
"name": "mtmharness",
- "version": "0.9.9",
+ "version": "0.9.10",
"description": "Unified DeepSeek Harness Web plugin with Connect, Codebase Memory, Modern Go, Ponytail, and independent static/embed clients.",
"type": "module",
"engines": { "node": ">=22.19.0", "pnpm": ">=11.7.0" },
@@ -12,6 +12,7 @@
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
"./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.cjs" },
"./embed": { "types": "./dist/types/standalone/index.d.ts", "import": "./dist/embed/mtmharness.js", "default": "./dist/embed/mtmharness.js" },
+ "./auth": { "types": "./dist/types/standalone/app/auth.d.ts", "import": "./dist/auth.js", "default": "./dist/auth.js" },
"./app": "./dist/standalone/index.html",
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
@@ -41,6 +42,7 @@
"lib/types/**/*.d.ts",
"dist/standalone",
"dist/embed",
+ "dist/auth.js",
"dist/types/standalone",
"cordis.patch.yml",
"README.md",
diff --git a/packages/mtmharness/scripts/build.mjs b/packages/mtmharness/scripts/build.mjs
index da3ad9d..ba6ec12 100644
--- a/packages/mtmharness/scripts/build.mjs
+++ b/packages/mtmharness/scripts/build.mjs
@@ -33,6 +33,17 @@ await build({
logLevel: "info",
});
+await build({
+ entryPoints: [resolve(packageRoot, "standalone/src/app/auth.ts")],
+ outfile: resolve(distRoot, "auth.js"),
+ bundle: true,
+ format: "esm",
+ platform: "browser",
+ target: "es2020",
+ legalComments: "none",
+ logLevel: "info",
+});
+
const clientBuild = await build({
entryPoints: [resolve(packageRoot, "src/client/index.ts")],
outfile: clientTemp,
diff --git a/packages/mtmharness/src/client/index.test.ts b/packages/mtmharness/src/client/index.test.ts
index 4860976..6f1a5b6 100644
--- a/packages/mtmharness/src/client/index.test.ts
+++ b/packages/mtmharness/src/client/index.test.ts
@@ -136,6 +136,7 @@ describe("mtmharness Host half", () => {
it("registers the Connect settings namespace without a local backend", async () => {
const { registeredNamespaces, cleanups } = await hostBench();
expect(registeredNamespaces).toContain("mtm-connect");
+ expect(registeredNamespaces).toContain("mtm-admin");
for (const cleanup of cleanups.reverse()) await cleanup();
});
@@ -181,6 +182,7 @@ describe("mtmharness browser half", () => {
expect(registered).toEqual(expect.arrayContaining([
expect.objectContaining({ name: "settings.plugin.item", options: expect.objectContaining({ key: "mtm-coding" }) }),
expect.objectContaining({ name: "settings.plugin.item", options: expect.objectContaining({ key: "mtm-connect" }) }),
+ expect.objectContaining({ name: "settings.plugin.item", options: expect.objectContaining({ key: "mtm-admin" }) }),
]));
expect(registered.filter((entry) => entry.name === "sidebar.footer.action")).toHaveLength(0);
expect(registered.filter((entry) => entry.name === "shell.overlay")).toHaveLength(0);
diff --git a/packages/mtmharness/src/client/index.ts b/packages/mtmharness/src/client/index.ts
index b1ce7e1..f0c596f 100644
--- a/packages/mtmharness/src/client/index.ts
+++ b/packages/mtmharness/src/client/index.ts
@@ -6,6 +6,7 @@ import type {} from "@deepseek-ai/dsh-client-ui-settings-plugins/client";
import type {} from "@deepseek-ai/dsh-client-ui-sidebar/client";
import { apply as applyCoding } from "../features/coding/client/index.tsx";
import { apply as applyMtmConnect } from "../features/mtm-connect/client/index.tsx";
+import { apply as applyMtmAdmin } from "../features/mtm-admin/client/index.tsx";
import { apply as applySecondary } from "../features/secondary/client.ts";
export { applyCoding };
@@ -15,5 +16,6 @@ export const inject = ["slots", "locale", "settingsScope", "connection"];
export function apply(ctx: ClientContext): void {
applyCoding(ctx);
applyMtmConnect(ctx);
+ applyMtmAdmin(ctx);
applySecondary(ctx);
}
diff --git a/packages/mtmharness/src/features/mtm-admin/client/MtmAdminCard.tsx b/packages/mtmharness/src/features/mtm-admin/client/MtmAdminCard.tsx
new file mode 100644
index 0000000..c29f7ce
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/client/MtmAdminCard.tsx
@@ -0,0 +1,78 @@
+import { useState, type CSSProperties } from "react";
+import type { InjectFace, PropsLocale, PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
+import type { MtmAdminCardFace, MtmAdminCardState } from "./controller.js";
+import type { MtmAdminLocaleKey } from "./locales.js";
+import type {} from "@deepseek-ai/dsh-client-ui-settings-plugins/client";
+
+export type MtmAdminCardProps =
+ PropsRuntime<"settings.plugin.item">
+ & PropsLocale<"mtm.admin">
+ & InjectFace;
+
+const cardStyle: CSSProperties = {
+ border: "1px solid color-mix(in srgb, currentColor 16%, transparent)",
+ borderRadius: 6,
+ listStyle: "none",
+ margin: "0 0 12px",
+ overflow: "hidden",
+};
+const headerStyle: CSSProperties = {
+ alignItems: "center",
+ background: "transparent",
+ border: 0,
+ color: "inherit",
+ cursor: "pointer",
+ display: "flex",
+ justifyContent: "space-between",
+ padding: "12px 14px",
+ textAlign: "left",
+ width: "100%",
+};
+const bodyStyle: CSSProperties = { borderTop: "1px solid color-mix(in srgb, currentColor 12%, transparent)", padding: "12px 14px 14px" };
+const actionStyle: CSSProperties = { display: "flex", flexWrap: "wrap", gap: 8, justifyContent: "flex-end", paddingTop: 14 };
+const buttonStyle: CSSProperties = { border: "1px solid color-mix(in srgb, currentColor 22%, transparent)", borderRadius: 4, cursor: "pointer", padding: "6px 10px" };
+
+function statusLabel(t: (key: MtmAdminLocaleKey) => string, state: MtmAdminCardState): string {
+ if (state.status === "disabled") return t("statusDisabled");
+ if (state.status === "loading") return t("statusLoading");
+ if (state.status === "failed") return t("statusFailed");
+ return t("statusEnabled");
+}
+
+export function MtmAdminCard(props: MtmAdminCardProps) {
+ const t = props.t;
+ const state = props.useMtmAdminCard((snapshot: MtmAdminCardState) => snapshot);
+ const [open, setOpen] = useState(false);
+ if (!state.available) return null;
+ const disabled = !state.writable;
+ return (
+
+
+ {open ? (
+
+ {disabled ?
{t("readOnly")}
: null}
+ {state.error ?
{state.error}
: null}
+
+
{t("enabledHint")}
+
+ {state.dirty ? {t("unsaved")} : null}
+ {state.failed ? {t("saveFailed")} : null}
+
+ {state.overridden ? : null}
+
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/packages/mtmharness/src/features/mtm-admin/client/controller.test.ts b/packages/mtmharness/src/features/mtm-admin/client/controller.test.ts
new file mode 100644
index 0000000..e9c2e43
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/client/controller.test.ts
@@ -0,0 +1,57 @@
+// @vitest-environment jsdom
+
+import { describe, expect, it, vi } from "vitest";
+import { MtmAdminCardController } from "./controller.ts";
+
+type SettingsSnapshot = {
+ status: "ready";
+ value: { enabled: boolean };
+ base: { enabled: boolean };
+ user: Record;
+ writable: boolean;
+};
+
+function setup() {
+ let snapshot: SettingsSnapshot = { status: "ready", value: { enabled: false }, base: { enabled: false }, user: {}, writable: true };
+ const settingsListeners = new Set<() => void>();
+ const publish = (enabled: boolean) => {
+ snapshot = { ...snapshot, value: { enabled }, user: { enabled }, };
+ for (const listener of settingsListeners) listener();
+ };
+ const settings = {
+ getSnapshot: () => snapshot,
+ subscribe(listener: () => void) { settingsListeners.add(listener); return () => { settingsListeners.delete(listener); }; },
+ async set(_field: string, enabled: boolean) { publish(enabled); },
+ async unset() { snapshot = { ...snapshot, value: { enabled: false }, user: {} }; for (const listener of settingsListeners) listener(); },
+ };
+ let runtimeState = { desired: false, status: "disabled" as const };
+ const runtimeListeners = new Set<() => void>();
+ const runtime = {
+ getSnapshot: () => runtimeState,
+ subscribe(listener: () => void) { runtimeListeners.add(listener); return () => { runtimeListeners.delete(listener); }; },
+ setEnabled: vi.fn(async (enabled: boolean) => { runtimeState = { desired: enabled, status: enabled ? "enabled" : "disabled" }; for (const listener of runtimeListeners) listener(); }),
+ show: vi.fn(),
+ dispose: vi.fn(async () => undefined),
+ };
+ return { settings, runtime, publish };
+}
+
+describe("mtm-admin settings controller", () => {
+ it("keeps the launcher disabled until enabled, then saves, opens, and disposes", async () => {
+ const state = setup();
+ const controller = new MtmAdminCardController(state.settings as never, state.runtime as never);
+ const face = controller.inject();
+ await vi.waitFor(() => { expect(state.runtime.setEnabled).toHaveBeenCalledWith(false); });
+
+ face.edit(true);
+ expect(face.hooks.mtmAdminCard.getSnapshot()).toMatchObject({ enabled: true, dirty: true });
+ face.save();
+ await vi.waitFor(() => { expect(face.hooks.mtmAdminCard.getSnapshot()).toMatchObject({ enabled: true, dirty: false, failed: false }); });
+ expect(state.runtime.setEnabled).toHaveBeenCalledWith(true);
+
+ face.open();
+ expect(state.runtime.show).toHaveBeenCalledOnce();
+ await controller.dispose();
+ expect(state.runtime.dispose).toHaveBeenCalledOnce();
+ });
+});
diff --git a/packages/mtmharness/src/features/mtm-admin/client/controller.ts b/packages/mtmharness/src/features/mtm-admin/client/controller.ts
new file mode 100644
index 0000000..50331d6
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/client/controller.ts
@@ -0,0 +1,159 @@
+import { createSnapshotStore, type SnapshotStore } from "@deepseek-ai/dsh-client-store";
+import type { SettingsScope } from "@deepseek-ai/dsh-client-ui-settings/client";
+import type { MtmSecondaryClientRuntime, MtmSecondarySnapshot } from "../../secondary/client.js";
+import type { MtmAdminSettings } from "../index.js";
+
+export interface MtmAdminCardState {
+ readonly available: boolean;
+ readonly writable: boolean;
+ readonly enabled: boolean;
+ readonly overridden: boolean;
+ readonly dirty: boolean;
+ readonly saving: boolean;
+ readonly failed: boolean;
+ readonly status: MtmSecondarySnapshot["status"];
+ readonly error?: string;
+}
+
+export interface MtmAdminCardFace {
+ readonly hooks: { mtmAdminCard: SnapshotStore };
+ readonly edit: (enabled: boolean) => void;
+ readonly save: () => void;
+ readonly discard: () => void;
+ readonly reset: () => void;
+ readonly open: () => void;
+}
+
+type Staged = { readonly enabled: boolean; readonly clear: boolean };
+type AdminSettingsSnapshot = ReturnType["getSnapshot"]>;
+
+function resolvedEnabled(snapshot: AdminSettingsSnapshot): boolean {
+ return (snapshot.value as Record | undefined)?.enabled === true;
+}
+
+function baseEnabled(snapshot: AdminSettingsSnapshot): boolean {
+ return ((snapshot.base as Record | undefined)?.enabled ?? false) === true;
+}
+
+function userHasEnabled(snapshot: AdminSettingsSnapshot): boolean {
+ const user = snapshot.user as Record | undefined;
+ return user !== undefined && Object.hasOwn(user, "enabled");
+}
+
+/** Settings state and lifecycle controller for the Admin launcher. */
+export class MtmAdminCardController {
+ private staged: Staged | undefined;
+ private readonly store: SnapshotStore;
+ private saving = false;
+ private failed = false;
+ private disposed = false;
+ private readonly stopSettings: () => void;
+ private readonly stopRuntime: () => void;
+ private reconciling = Promise.resolve();
+
+ constructor(
+ private readonly scope: SettingsScope,
+ private readonly runtime: MtmSecondaryClientRuntime,
+ ) {
+ this.store = createSnapshotStore(this.projection());
+ this.stopSettings = scope.subscribe(() => {
+ this.publish();
+ void this.queueReconcile().catch(() => { this.failed = true; this.publish(); });
+ });
+ this.stopRuntime = runtime.subscribe(() => { this.publish(); });
+ void this.queueReconcile().catch(() => { this.failed = true; this.publish(); });
+ }
+
+ inject(): MtmAdminCardFace {
+ return {
+ hooks: { mtmAdminCard: this.store },
+ edit: (enabled) => { this.edit(enabled); },
+ save: () => { void this.save(); },
+ discard: () => { this.discard(); },
+ reset: () => { this.reset(); },
+ open: () => { this.runtime.show("[data-mtm-admin-launcher]"); },
+ };
+ }
+
+ async dispose(): Promise {
+ if (this.disposed) return;
+ this.disposed = true;
+ this.stopSettings();
+ this.stopRuntime();
+ await this.reconciling;
+ await this.runtime.dispose();
+ }
+
+ private edit(enabled: boolean): void {
+ this.staged = { enabled, clear: false };
+ this.failed = false;
+ this.publish();
+ }
+
+ private discard(): void {
+ this.staged = undefined;
+ this.failed = false;
+ this.publish();
+ }
+
+ private reset(): void {
+ this.staged = { enabled: baseEnabled(this.scope.getSnapshot()), clear: true };
+ this.failed = false;
+ this.publish();
+ }
+
+ private async save(): Promise {
+ const staged = this.staged;
+ if (this.saving || staged === undefined || !this.scope.getSnapshot().writable) return;
+ this.saving = true;
+ this.failed = false;
+ this.publish();
+ try {
+ if (staged.clear) await this.scope.unset("enabled");
+ else await this.scope.set("enabled", staged.enabled);
+ const snapshot = this.scope.getSnapshot();
+ if (staged.clear ? userHasEnabled(snapshot) : resolvedEnabled(snapshot) !== staged.enabled) {
+ throw new Error("MTM Admin setting was not accepted");
+ }
+ this.staged = undefined;
+ await this.queueReconcile();
+ } catch {
+ this.failed = true;
+ } finally {
+ this.saving = false;
+ this.publish();
+ }
+ }
+
+ private queueReconcile(): Promise {
+ const operation = this.reconciling.then(() => this.reconcile(), () => this.reconcile());
+ this.reconciling = operation.then(() => undefined, () => undefined);
+ return operation;
+ }
+
+ private async reconcile(): Promise {
+ if (this.disposed) return;
+ await this.runtime.setEnabled(resolvedEnabled(this.scope.getSnapshot()));
+ this.publish();
+ }
+
+ private projection(): MtmAdminCardState {
+ const snapshot = this.scope.getSnapshot();
+ const staged = this.staged;
+ return {
+ available: snapshot.status === "ready",
+ writable: snapshot.writable,
+ enabled: staged?.enabled ?? resolvedEnabled(snapshot),
+ overridden: staged?.clear === true ? false : staged !== undefined || userHasEnabled(snapshot),
+ dirty: staged !== undefined,
+ saving: this.saving,
+ failed: this.failed,
+ status: this.runtime.getSnapshot().status,
+ error: this.runtime.getSnapshot().error,
+ };
+ }
+
+ private publish(): void {
+ if (!this.disposed) this.store.set(this.projection());
+ }
+}
diff --git a/packages/mtmharness/src/features/mtm-admin/client/index.tsx b/packages/mtmharness/src/features/mtm-admin/client/index.tsx
new file mode 100644
index 0000000..7221125
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/client/index.tsx
@@ -0,0 +1,37 @@
+import type { Context as ClientContext } from "@deepseek-ai/cordis";
+import type {} from "@deepseek-ai/dsh-client-locale/client";
+import type {} from "@deepseek-ai/dsh-client-ui-renderer/client";
+import type {} from "@deepseek-ai/dsh-client-ui-settings-plugins/client";
+import type {} from "@deepseek-ai/dsh-client-ui-slots";
+import { MtmSecondaryClientRuntime } from "../../secondary/client.js";
+import { MTM_ADMIN_EXTENSION } from "../../secondary/manifest.js";
+import { MtmAdminCard } from "./MtmAdminCard.js";
+import { MtmAdminCardController } from "./controller.js";
+import { en, zh, type MtmAdminLocaleKey } from "./locales.js";
+import { SETTINGS_NAMESPACE } from "../contract.js";
+import type { MtmAdminSettings } from "../index.js";
+
+declare module "@deepseek-ai/dsh-client-ui-slots" {
+ interface LocaleNamespaceMap {
+ "mtm.admin": MtmAdminLocaleKey;
+ }
+}
+
+export const name = "mtm-admin-client";
+export const inject = ["slots", "locale", "settingsScope"];
+
+/** Register the Admin launcher settings card and runtime extension. */
+export function apply(ctx: ClientContext): void {
+ const settings = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
+ const runtime = new MtmSecondaryClientRuntime({ document: typeof document === "undefined" ? undefined : document }, MTM_ADMIN_EXTENSION);
+ const controller = new MtmAdminCardController(settings, runtime);
+ ctx.effect(() => async () => { await controller.dispose(); }, "mtm-admin: client lifecycle");
+ const t = ctx.locale.bind("mtm.admin");
+ ctx.effect(() => ctx.locale.register("mtm.admin", { en, zh }), "mtm-admin: locale");
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
+ name: "settings.plugin.item",
+ key: SETTINGS_NAMESPACE,
+ locale: "mtm.admin",
+ inject: () => controller.inject(),
+ }, (props) => ));
+}
diff --git a/packages/mtmharness/src/features/mtm-admin/client/locales.ts b/packages/mtmharness/src/features/mtm-admin/client/locales.ts
new file mode 100644
index 0000000..92a664e
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/client/locales.ts
@@ -0,0 +1,61 @@
+export type MtmAdminLocaleKey =
+ | "title"
+ | "description"
+ | "enabled"
+ | "enabledHint"
+ | "reset"
+ | "statusDisabled"
+ | "statusLoading"
+ | "statusEnabled"
+ | "statusFailed"
+ | "open"
+ | "save"
+ | "saving"
+ | "discard"
+ | "unsaved"
+ | "saveFailed"
+ | "readOnly"
+ | "show"
+ | "hide";
+
+export const en: Record = {
+ title: "MTM Admin",
+ description: "Independent gomtm control-plane application.",
+ enabled: "Enabled",
+ enabledHint: "Load the pinned Admin launcher at runtime.",
+ reset: "Reset",
+ statusDisabled: "Disabled",
+ statusLoading: "Loading",
+ statusEnabled: "Ready",
+ statusFailed: "Failed",
+ open: "Open Admin",
+ save: "Save",
+ saving: "Saving...",
+ discard: "Discard",
+ unsaved: "Unsaved",
+ saveFailed: "The setting could not be saved; your edit was kept.",
+ readOnly: "This deployment stores settings read-only.",
+ show: "Show settings",
+ hide: "Hide settings",
+};
+
+export const zh: Record = {
+ title: "MTM 管理员",
+ description: "独立的 gomtm 控制面应用。",
+ enabled: "启用",
+ enabledHint: "运行时加载固定版本的 Admin 入口。",
+ reset: "恢复默认",
+ statusDisabled: "已禁用",
+ statusLoading: "加载中",
+ statusEnabled: "就绪",
+ statusFailed: "失败",
+ open: "打开 Admin",
+ save: "保存",
+ saving: "保存中...",
+ discard: "放弃修改",
+ unsaved: "未保存",
+ saveFailed: "设置保存失败,修改仍保留供你修正。",
+ readOnly: "本部署的设置为只读。",
+ show: "展开设置",
+ hide: "收起设置",
+};
diff --git a/packages/mtmharness/src/features/mtm-admin/contract.ts b/packages/mtmharness/src/features/mtm-admin/contract.ts
new file mode 100644
index 0000000..b8037e7
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/contract.ts
@@ -0,0 +1 @@
+export const SETTINGS_NAMESPACE = "mtm-admin";
diff --git a/packages/mtmharness/src/features/mtm-admin/index.test.ts b/packages/mtmharness/src/features/mtm-admin/index.test.ts
new file mode 100644
index 0000000..5ef10ed
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/index.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, it } from "vitest";
+import { apply, MtmAdminSettingsSchema, SETTINGS_NAMESPACE } from "./index.ts";
+
+describe("mtm-admin Host settings", () => {
+ it("registers a disabled-by-default namespace", () => {
+ let registration: { namespace: unknown; schema: unknown; options: unknown } | undefined;
+ apply({
+ settings: {
+ register(namespace: unknown, schema: unknown, options: unknown) {
+ registration = { namespace, schema, options };
+ return {};
+ },
+ },
+ } as never);
+ expect(registration).toMatchObject({ namespace: SETTINGS_NAMESPACE, schema: MtmAdminSettingsSchema, options: { base: { enabled: false } } });
+ expect(MtmAdminSettingsSchema({})).toMatchObject({ enabled: false });
+ });
+});
diff --git a/packages/mtmharness/src/features/mtm-admin/index.ts b/packages/mtmharness/src/features/mtm-admin/index.ts
new file mode 100644
index 0000000..d89e971
--- /dev/null
+++ b/packages/mtmharness/src/features/mtm-admin/index.ts
@@ -0,0 +1,23 @@
+import type { Context } from "@deepseek-ai/cordis";
+import type {} from "@deepseek-ai/dsh-settings";
+import z from "@deepseek-ai/schemastery";
+import { SETTINGS_NAMESPACE } from "./contract.ts";
+export { SETTINGS_NAMESPACE } from "./contract.ts";
+
+export interface MtmAdminSettings {
+ enabled: boolean;
+}
+
+export type MtmAdminConfig = Partial;
+
+export const MtmAdminSettingsSchema: z = z.object({
+ enabled: z.boolean().default(false),
+});
+
+export const name = "mtm-admin";
+export const inject = ["settings"];
+
+/** Register the user-owned setting for the token-free Admin launcher. */
+export function apply(ctx: Context, rawConfig: MtmAdminConfig = {}): void {
+ ctx.settings.register(SETTINGS_NAMESPACE, MtmAdminSettingsSchema, { base: { enabled: rawConfig.enabled ?? false } });
+}
diff --git a/packages/mtmharness/src/features/secondary/manifest.ts b/packages/mtmharness/src/features/secondary/manifest.ts
index bdc17c0..6ec4f74 100644
--- a/packages/mtmharness/src/features/secondary/manifest.ts
+++ b/packages/mtmharness/src/features/secondary/manifest.ts
@@ -61,3 +61,12 @@ export const MTM_CONNECT_EXTENSION = {
clientUrl: "https://unpkg.com/mtm-connect@0.2.0/lib/client.js",
clientIntegrity: "sha256-DS/tRWnWzx1IqccuJApF8IVgh5lv9fhP1CyNQUI+nCw=",
} as const satisfies MtmSecondaryExtensionManifest;
+
+/** The published token-free Admin launcher for the independent control plane. */
+export const MTM_ADMIN_EXTENSION = {
+ apiVersion: 1,
+ id: "mtm-admin",
+ version: "0.1.0",
+ clientUrl: "https://unpkg.com/mtm-admin@0.1.0/lib/client.js",
+ clientIntegrity: "sha256-MRsPUnazjbyWqvKS1A607z7Oz+YsRiZaw8aShroohTM=",
+} as const satisfies MtmSecondaryExtensionManifest;
diff --git a/packages/mtmharness/src/index.ts b/packages/mtmharness/src/index.ts
index 6027444..a382d10 100644
--- a/packages/mtmharness/src/index.ts
+++ b/packages/mtmharness/src/index.ts
@@ -3,6 +3,7 @@ import type { Context } from "@deepseek-ai/cordis";
import type {} from "@deepseek-ai/dsh-client-connection";
import { apply as applyCodingHost } from "./features/coding/index.ts";
import { apply as applyMtmConnectSettings } from "./features/mtm-connect/index.ts";
+import { apply as applyMtmAdminSettings } from "./features/mtm-admin/index.ts";
import { apply as applyUpdateHost } from "./features/update/index.ts";
export { buildMcpConfig, codingPackage, resolveConfig, MTM_CODING_PACKAGES } from "./features/coding/index.ts";
@@ -17,6 +18,8 @@ export {
export { apply as applyCoding } from "./features/coding/index.ts";
export { MtmConnectSettingsSchema, SETTINGS_NAMESPACE as MTM_CONNECT_SETTINGS_NAMESPACE } from "./features/mtm-connect/index.ts";
export type { MtmConnectConfig, MtmConnectSettings } from "./features/mtm-connect/index.ts";
+export { MtmAdminSettingsSchema, SETTINGS_NAMESPACE as MTM_ADMIN_SETTINGS_NAMESPACE } from "./features/mtm-admin/index.ts";
+export type { MtmAdminConfig, MtmAdminSettings } from "./features/mtm-admin/index.ts";
export { apply as applyCodebaseMemory } from "./features/coding/codebase-memory.ts";
export { apply as applyPonytail } from "./features/coding/ponytail.ts";
export { apply as applyRtk } from "./features/coding/rtk.ts";
@@ -49,6 +52,7 @@ export const inject = ["connection", "settings", "subprocess"];
export async function apply(ctx: Context, config: Record = {}): Promise {
if (ctx.connection === undefined) throw new Error("mtmharness: DSH connection service is unavailable");
applyMtmConnectSettings(ctx, typeof config["mtm-connect"] === "object" && config["mtm-connect"] !== null ? config["mtm-connect"] as { enabled?: boolean } : {});
+ applyMtmAdminSettings(ctx, typeof config["mtm-admin"] === "object" && config["mtm-admin"] !== null ? config["mtm-admin"] as { enabled?: boolean } : {});
applyUpdateHost(ctx);
await applyCodingHost(ctx, config);
}
diff --git a/packages/mtmharness/tests/package-contract.test.ts b/packages/mtmharness/tests/package-contract.test.ts
index 1803603..c7e28bd 100644
--- a/packages/mtmharness/tests/package-contract.test.ts
+++ b/packages/mtmharness/tests/package-contract.test.ts
@@ -11,6 +11,7 @@ describe("mtmharness package contract", () => {
".": { default?: string };
"./client": { default?: string };
"./embed": { import?: string };
+ "./auth": { import?: string };
"./app": string;
};
unpkg?: string;
@@ -23,6 +24,7 @@ describe("mtmharness package contract", () => {
expect(manifest.exports["."]?.default).toBe("./lib/index.js");
expect(manifest.exports["./client"]?.default).toBe("./lib/client.cjs");
expect(manifest.exports["./embed"]?.import).toBe("./dist/embed/mtmharness.js");
+ expect(manifest.exports["./auth"]?.import).toBe("./dist/auth.js");
expect(manifest.exports["./app"]).toBe("./dist/standalone/index.html");
expect(manifest.unpkg).toBe("./dist/embed/mtmharness.iife.js");
expect(manifest.jsdelivr).toBe("./dist/embed/mtmharness.iife.js");
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 566f699..89ab615 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,6 +12,72 @@ importers:
specifier: 6.0.3
version: 6.0.3
+ packages/mtm-admin:
+ devDependencies:
+ '@base-ui/react':
+ specifier: 1.7.0
+ version: 1.7.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@tailwindcss/vite':
+ specifier: 4.3.3
+ version: 4.3.3(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))
+ '@testing-library/react':
+ specifier: 16.3.2
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@types/node':
+ specifier: 22.20.1
+ version: 22.20.1
+ '@types/react':
+ specifier: ~18.3.1
+ version: 18.3.31
+ '@types/react-dom':
+ specifier: ~18.3.0
+ version: 18.3.7(@types/react@18.3.31)
+ '@vitejs/plugin-react':
+ specifier: 6.0.5
+ version: 6.0.5(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))
+ class-variance-authority:
+ specifier: 0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: 2.1.1
+ version: 2.1.1
+ esbuild:
+ specifier: 0.28.2
+ version: 0.28.2
+ jsdom:
+ specifier: 30.0.1
+ version: 30.0.1
+ lucide-react:
+ specifier: 1.21.0
+ version: 1.21.0(react@18.3.1)
+ mtmharness:
+ specifier: workspace:*
+ version: link:../mtmharness
+ react:
+ specifier: 18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: 18.3.1
+ version: 18.3.1(react@18.3.1)
+ tailwind-merge:
+ specifier: 3.6.0
+ version: 3.6.0
+ tailwindcss:
+ specifier: 4.3.0
+ version: 4.3.0
+ tw-animate-css:
+ specifier: 1.4.0
+ version: 1.4.0
+ typescript:
+ specifier: 6.0.3
+ version: 6.0.3
+ vite:
+ specifier: 8.2.2
+ version: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)
+ vitest:
+ specifier: 4.1.11
+ version: 4.1.11(@types/node@22.20.1)(jsdom@30.0.1)(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))
+
packages/mtm-connect:
devDependencies:
'@types/node':
@@ -257,6 +323,14 @@ packages:
resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==}
engines: {node: ^22.13.0 || >=24.0.0}
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/runtime@7.29.7':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
@@ -1059,6 +1133,28 @@ packages:
'@tanstack/store@0.9.3':
resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/react@16.3.2':
+ resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -1163,6 +1259,17 @@ packages:
anser@2.3.5:
resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==}
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -1295,6 +1402,9 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1673,6 +1783,10 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1969,6 +2083,10 @@ packages:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
property-information@7.2.0:
resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
@@ -1997,6 +2115,9 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -2398,6 +2519,14 @@ snapshots:
is-potential-custom-element-name: 1.0.1
lru-cache: 11.5.2
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
'@babel/runtime@7.29.7': {}
'@base-ui/react@1.7.0(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
@@ -3073,6 +3202,29 @@ snapshots:
'@tanstack/store@0.9.3': {}
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/runtime': 7.29.7
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@testing-library/dom': 10.4.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.31
+ '@types/react-dom': 18.3.7(@types/react@18.3.31)
+
+ '@types/aria-query@5.0.4': {}
+
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -3181,6 +3333,14 @@ snapshots:
anser@2.3.5: {}
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@5.2.0: {}
+
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
assertion-error@2.0.1: {}
bidi-js@1.0.3:
@@ -3296,6 +3456,8 @@ snapshots:
dependencies:
dequal: 2.0.3
+ dom-accessibility-api@0.5.16: {}
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3673,6 +3835,8 @@ snapshots:
dependencies:
react: 18.3.1
+ lz-string@1.5.0: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -4076,6 +4240,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
property-information@7.2.0: {}
proxy-addr@2.0.7:
@@ -4105,6 +4275,8 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-is@17.0.2: {}
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0