fix: rebuild HWPOD native L1 shell
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseHwpodNativeAccessProfile } from "./hwpod-native-vite.config";
|
||||
import { firstAllowedNavPath } from "../src/stores/auth";
|
||||
|
||||
describe("HWPOD native Cloud Console profile", () => {
|
||||
it("accepts the YAML-rendered HWPOD-only navigation profile", () => {
|
||||
const profile = parseHwpodNativeAccessProfile(JSON.stringify({
|
||||
id: "hwpod-native-l1",
|
||||
allowedNavIds: ["admin.hwpodGroups"],
|
||||
startPath: "/hwpods/devices",
|
||||
}));
|
||||
expect(profile).toEqual({ id: "hwpod-native-l1", allowedNavIds: ["admin.hwpodGroups"], startPath: "/hwpods/devices" });
|
||||
expect(firstAllowedNavPath(profile.allowedNavIds)).toBe(profile.startPath);
|
||||
});
|
||||
|
||||
it("fails closed without a declared navigation profile", () => {
|
||||
expect(() => parseHwpodNativeAccessProfile(undefined)).toThrow(/HWPOD_WEB_ACCESS_PROFILE_JSON/u);
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,67 @@
|
||||
/*
|
||||
* SPEC: PJ2026-010103 HWPOD 服务。
|
||||
* 实现引用: draft-2026-07-20-hwpod-temporal-split。
|
||||
* 责任: 独立 HWPOD L1 Web 的固定端口、健康检查和 API proxy。
|
||||
* 责任: 复用完整 Cloud Console 壳的 HWPOD L1 Web、原生会话和 API proxy。
|
||||
*/
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import type { ServerResponse } from "node:http";
|
||||
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
|
||||
export default defineConfig(({ command }) => ({
|
||||
root: path.resolve(import.meta.dirname, "../hwpod"),
|
||||
build: { outDir: "dist", emptyOutDir: true },
|
||||
root: fileURLToPath(new URL("..", import.meta.url)),
|
||||
cacheDir: ".state/vite/hwpod",
|
||||
resolve: { alias: { "@": fileURLToPath(new URL("../src", import.meta.url)) } },
|
||||
build: { outDir: "hwpod/dist", emptyOutDir: true },
|
||||
server: command === "serve" ? {
|
||||
host: process.env.HWPOD_WEB_HOST || "0.0.0.0",
|
||||
host: requiredString(process.env.HWPOD_WEB_HOST, "HWPOD_WEB_HOST"),
|
||||
port: requiredPort(process.env.HWPOD_WEB_PORT),
|
||||
strictPort: true,
|
||||
proxy: { "/v1": { target: requiredApiUrl(), changeOrigin: false }, "/health/ready": { target: requiredApiUrl(), changeOrigin: false } }
|
||||
proxy: {
|
||||
"/v1/hwpod": { target: requiredApiUrl(), changeOrigin: false },
|
||||
"/health/ready": { target: requiredApiUrl(), changeOrigin: false },
|
||||
}
|
||||
} : undefined,
|
||||
plugins: [healthPlugin()]
|
||||
plugins: command === "serve"
|
||||
? [nativeSessionPlugin(process.env.HWPOD_WEB_ACCESS_PROFILE_JSON), healthPlugin(), vue()]
|
||||
: [vue()]
|
||||
}));
|
||||
|
||||
function healthPlugin(): Plugin { return { name: "hwpod-native-health", configureServer(server) { server.middlewares.use((request, response, next) => { if (request.url !== "/health/live") return next(); response.statusCode = 200; response.setHeader("content-type", "application/json"); response.end(JSON.stringify({ ok: true, serviceId: "hwlab-hwpod-web", status: "live", valuesPrinted: false })); }); } }; }
|
||||
function nativeSessionPlugin(serialized: string | undefined): Plugin {
|
||||
const profile = parseHwpodNativeAccessProfile(serialized);
|
||||
const session = {
|
||||
authenticated: true,
|
||||
authMethod: "native-development",
|
||||
identityAuthority: "yaml-native-profile",
|
||||
sessionKind: "native-hwpod",
|
||||
user: { id: "native-hwpod", username: "native-hwpod", displayName: "HWPOD Native", role: "admin", status: "active" },
|
||||
capabilities: { admin: "available" },
|
||||
access: { nav: { profileId: profile.id, allowedIds: profile.allowedNavIds, valuesRedacted: true } },
|
||||
valuesRedacted: true,
|
||||
};
|
||||
return { name: "hwpod-native-session", configureServer(server) { server.middlewares.use((request, response, next) => {
|
||||
const pathname = new URL(request.url ?? "/", "http://hwpod-native").pathname;
|
||||
if (pathname === "/auth/session" && request.method === "GET") return sendJson(response, 200, session);
|
||||
if (pathname === "/auth/logout" && request.method === "POST") return sendJson(response, 200, { ok: true, authenticated: true, valuesRedacted: true });
|
||||
return next();
|
||||
}); } };
|
||||
}
|
||||
export function parseHwpodNativeAccessProfile(serialized: string | undefined) {
|
||||
let value: unknown;
|
||||
try { value = JSON.parse(requiredString(serialized, "HWPOD_WEB_ACCESS_PROFILE_JSON")); }
|
||||
catch (error) { throw new Error(`HWPOD_WEB_ACCESS_PROFILE_JSON must be valid JSON: ${error instanceof Error ? error.message : String(error)}`); }
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("HWPOD_WEB_ACCESS_PROFILE_JSON must be an object");
|
||||
const raw = value as Record<string, unknown>;
|
||||
const id = requiredString(raw.id, "HWPOD_WEB_ACCESS_PROFILE_JSON.id");
|
||||
const startPath = requiredString(raw.startPath, "HWPOD_WEB_ACCESS_PROFILE_JSON.startPath");
|
||||
const allowedNavIds = Array.isArray(raw.allowedNavIds) ? raw.allowedNavIds.map((item) => requiredString(item, "HWPOD_WEB_ACCESS_PROFILE_JSON.allowedNavIds[]")) : [];
|
||||
if (allowedNavIds.length === 0) throw new Error("HWPOD_WEB_ACCESS_PROFILE_JSON.allowedNavIds must not be empty");
|
||||
if (!startPath.startsWith("/")) throw new Error("HWPOD_WEB_ACCESS_PROFILE_JSON.startPath must be an absolute application path");
|
||||
return { id, allowedNavIds, startPath };
|
||||
}
|
||||
function sendJson(response: ServerResponse, status: number, body: unknown) { response.statusCode = status; response.setHeader("content-type", "application/json"); response.setHeader("cache-control", "no-store"); response.end(JSON.stringify(body)); }
|
||||
function requiredApiUrl() { const value = process.env.HWPOD_NATIVE_API_URL; if (!value) throw new Error("HWPOD_NATIVE_API_URL is required"); return value; }
|
||||
function requiredPort(value: string | undefined) { const port = Number.parseInt(String(value ?? ""), 10); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("HWPOD_WEB_PORT is required and must be valid"); return port; }
|
||||
function requiredString(value: unknown, name: string) { const text = typeof value === "string" ? value.trim() : ""; if (!text) throw new Error(`${name} is required`); return text; }
|
||||
|
||||
@@ -138,8 +138,9 @@ function accessFromUnknown(value: unknown): AuthAccess | undefined {
|
||||
return { nav: { profileId: nonEmptyString(nav.profileId) ?? undefined, allowedIds, valuesRedacted: nav.valuesRedacted === true } };
|
||||
}
|
||||
|
||||
function firstAllowedNavPath(allowedIds: string[]): string {
|
||||
export function firstAllowedNavPath(allowedIds: string[]): string {
|
||||
const ordered = [
|
||||
{ id: "admin.hwpodGroups", path: "/hwpods/devices" },
|
||||
{ id: "workbench.code", path: "/workbench" },
|
||||
{ id: "workbench.debug", path: "/workbench/debug" },
|
||||
{ id: "opencode.root", path: "/opencode" },
|
||||
|
||||
Reference in New Issue
Block a user