feat(device-manager): add standalone project workspace

This commit is contained in:
Codex
2026-08-13 11:37:36 +03:00
parent 6c1265b8b6
commit 4116f5ba95
17 changed files with 2584 additions and 0 deletions
@@ -0,0 +1,232 @@
import { randomUUID } from "node:crypto";
const commandRoutes = new Map([
["owner-scopes:ensure", "/internal/v1/management/owner-scopes:ensure"],
["projects:ensure", "/internal/v1/management/projects:ensure"],
["collections:ensure", "/internal/v1/management/collections:ensure"],
["devices:claim", "/internal/v1/management/devices:claim"],
]);
export function createDeviceCoreClient({ baseUrl, token, fetchImpl = fetch } = {}) {
const endpoint = normalizeBaseUrl(baseUrl);
if (typeof token !== "string" || token.length < 32) {
throw serviceError("device_core_token_invalid", 503);
}
async function request(pathname, actor, init = {}) {
const response = await fetchImpl(new URL(pathname, endpoint), {
...init,
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
...actorHeaders(actor),
...(init.headers ?? {}),
},
signal: AbortSignal.timeout(10_000),
});
const body = await response.json().catch(() => null);
if (!response.ok || body?.ok !== true) {
throw serviceError(
safeCoreError(body?.error),
response.status >= 400 && response.status < 600 ? response.status : 502,
);
}
return body;
}
return {
configured: true,
async listProjects(actor) {
return request("/internal/v1/query/projects", actor)
.then((body) => body.projects);
},
async getWorkspace(actor, projectRef) {
const projectId = entityId(projectRef, "project");
return request(`/internal/v1/query/projects/${projectId}/workspace`, actor)
.then((body) => body.workspace);
},
async execute(command, actor, input, idempotencyKey) {
const pathname = commandRoutes.get(command);
if (!pathname) throw serviceError("device_manager_command_invalid", 404);
if (!/^[\x21-\x7e]{8,256}$/.test(idempotencyKey || "")) {
throw serviceError("device_idempotency_key_invalid", 400);
}
return request(pathname, actor, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(input),
}).then(({ replayed, result }) => ({ replayed, result }));
},
};
}
export function createLocalPreviewDeviceCore() {
const ownerScopes = new Map();
const projects = new Map();
const collections = new Map();
function projectSummary(project) {
const projectCollections = [...collections.values()]
.filter((collection) => collection.projectRef === project.projectRef);
return {
...project,
counts: { devices: 0, collections: projectCollections.length, discoveries: 0 },
};
}
return {
configured: true,
async listProjects() {
return [...projects.values()].map(projectSummary);
},
async getWorkspace(_actor, projectRef) {
const project = projects.get(projectRef);
if (!project) throw serviceError("device_project_not_found", 404);
return {
project: projectSummary(project),
devices: [],
discoveries: [],
enrollments: [],
collections: [...collections.values()]
.filter((collection) => collection.projectRef === projectRef)
.map(({ projectRef: _projectRef, ...collection }) => collection),
};
},
async execute(command, actor, input) {
if (command === "owner-scopes:ensure") {
const key = `${input.scopeKind}:${input.ownerRef}`;
const created = !ownerScopes.has(key);
const scope = {
ownerScopeRef: ownerScopes.get(key)?.ownerScopeRef || `owner-scope:${randomUUID()}`,
scopeKind: input.scopeKind,
ownerRef: input.ownerRef,
displayName: input.displayName,
lifecycleState: "active",
};
ownerScopes.set(key, scope);
return { replayed: false, result: { created, ownerScope: scope } };
}
if (command === "projects:ensure") {
const scope = ownerScopes.get(`${input.scopeKind}:${input.ownerRef}`);
if (!scope) throw serviceError("device_owner_scope_not_found", 404);
const existing = [...projects.values()].find((project) =>
project.ownerScope.ownerRef === input.ownerRef
&& project.projectKey === input.projectKey
);
const projectRef = existing?.projectRef || `project:${randomUUID()}`;
const project = {
projectRef,
projectKey: input.projectKey,
name: input.name,
description: input.description ?? null,
lifecycleState: "active",
ownerScope: scope,
access: { projectRole: "owner", capabilities: ownerCapabilities },
counts: { devices: 0, collections: 0, discoveries: 0 },
createdAt: existing?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
projects.set(projectRef, project);
return { replayed: false, result: { created: !existing, project } };
}
if (command === "collections:ensure") {
const projectRef = input.projectRef;
if (!projects.has(projectRef)) throw serviceError("device_project_not_found", 404);
const existing = [...collections.values()].find((collection) =>
collection.projectRef === projectRef
&& collection.collectionKey === input.collectionKey
);
const collectionRef = existing?.collectionRef || `collection:${randomUUID()}`;
const collection = {
collectionRef,
projectRef,
collectionKey: input.collectionKey,
name: input.name,
description: input.description ?? null,
lifecycleState: "active",
memberCount: existing?.memberCount || 0,
createdAt: existing?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
collections.set(collectionRef, collection);
return { replayed: false, result: { created: !existing, collection } };
}
if (command === "devices:claim") {
throw serviceError("device_discovery_not_found", 404);
}
throw serviceError("device_manager_command_invalid", 404);
},
snapshot() {
return { ownerScopes, projects, collections };
},
};
}
const ownerCapabilities = Object.freeze([
"project.read",
"project.manage",
"access.manage",
"inventory.read",
"device.enroll",
"device.claim",
"device.transfer",
"collection.manage",
"route.manage",
"binding.manage",
"telemetry.observe",
"configuration.read",
"configuration.manage",
"command.plan",
"command.confirm",
"command.dispatch",
"credential.manage",
"audit.read",
]);
function actorHeaders(actor) {
if (!actor || typeof actor !== "object") throw serviceError("device_actor_required", 401);
return {
"X-NODEDC-User-Ref": actor.userRef,
"X-NODEDC-Hub-Role": actor.hubRole,
"X-NODEDC-Group-Refs": (actor.groupRefs ?? []).join(","),
"X-NODEDC-Owner-Scopes": (actor.ownerScopes ?? [])
.map((scope) => `${scope.scopeKind}=${scope.ownerRef}`)
.join(","),
};
}
function entityId(value, prefix) {
const match = String(value || "").match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw serviceError(`device_${prefix}_ref_invalid`, 400);
return match[1].toLowerCase();
}
function normalizeBaseUrl(value) {
if (typeof value !== "string" || value.trim() === "") {
throw serviceError("device_core_url_required", 503);
}
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
throw serviceError("device_core_url_invalid", 503);
}
url.pathname = url.pathname.replace(/\/$/, "") || "/";
return url;
}
function safeCoreError(value) {
return typeof value === "string" && /^device_[a-z0-9._:-]{2,120}$/.test(value)
? value
: "device_core_unavailable";
}
function serviceError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
createDeviceCoreClient,
createLocalPreviewDeviceCore,
} from "./device-core-client.mjs";
const token = "device-core-test-token-that-is-never-exposed";
const actor = Object.freeze({
userRef: "user:device-admin",
hubRole: "admin",
groupRefs: ["group:device-engineers"],
ownerScopes: [{ scopeKind: "personal", ownerRef: "user:device-admin" }],
});
test("Device Core client creates trusted actor headers and keeps its token server-side", async () => {
const calls = [];
const client = createDeviceCoreClient({
baseUrl: "http://device-control-core:3210",
token,
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init });
return jsonResponse(200, { ok: true, projects: [] });
},
});
assert.deepEqual(await client.listProjects(actor), []);
assert.equal(calls.length, 1);
assert.equal(calls[0].init.headers.Authorization, `Bearer ${token}`);
assert.equal(calls[0].init.headers["X-NODEDC-User-Ref"], actor.userRef);
assert.equal(calls[0].init.headers["X-NODEDC-Hub-Role"], "admin");
assert.equal(calls[0].init.headers["X-NODEDC-Group-Refs"], "group:device-engineers");
assert.equal(
calls[0].init.headers["X-NODEDC-Owner-Scopes"],
"personal=user:device-admin",
);
assert.equal(JSON.stringify(await client.listProjects(actor)).includes(token), false);
});
test("Device Core client accepts only canonical commands and entity refs", async () => {
const client = createDeviceCoreClient({
baseUrl: "http://127.0.0.1:3210",
token,
fetchImpl: async () => jsonResponse(200, { ok: true, replayed: false, result: {} }),
});
await assert.rejects(
client.execute("raw:proxy", actor, {}, "device-manager-12345678"),
/device_manager_command_invalid/,
);
await assert.rejects(
client.getWorkspace(actor, "project:not-a-uuid"),
/device_project_ref_invalid/,
);
await assert.rejects(
client.execute("projects:ensure", actor, {}, "short"),
/device_idempotency_key_invalid/,
);
});
test("local preview is empty and creates resources only through canonical commands", async () => {
const client = createLocalPreviewDeviceCore();
assert.deepEqual(await client.listProjects(actor), []);
const owner = await client.execute("owner-scopes:ensure", actor, {
scopeKind: "personal",
ownerRef: actor.userRef,
displayName: "Device Admin",
});
assert.equal(owner.result.created, true);
const created = await client.execute("projects:ensure", actor, {
scopeKind: "personal",
ownerRef: actor.userRef,
projectKey: "sandbox",
name: "Device sandbox",
description: null,
});
assert.equal(created.result.created, true);
const projectRef = created.result.project.projectRef;
await client.execute("collections:ensure", actor, {
projectRef,
collectionKey: "field-devices",
name: "Field devices",
description: null,
});
const projects = await client.listProjects(actor);
assert.equal(projects.length, 1);
assert.equal(projects[0].counts.collections, 1);
const workspace = await client.getWorkspace(actor, projectRef);
assert.equal(workspace.collections[0].collectionKey, "field-devices");
assert.deepEqual(workspace.devices, []);
});
function jsonResponse(status, body) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
@@ -0,0 +1,403 @@
import { randomBytes } from "node:crypto";
const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
const DEFAULT_VALIDATION_TTL_MS = 20_000;
const DEFAULT_VALIDATION_GRACE_MS = 30_000;
export function createDeviceManagerAuth({
env = process.env,
fetchImpl = fetch,
now = Date.now,
} = {}) {
const authRequired = booleanValue(
env.NODEDC_DEVICE_MANAGER_AUTH_REQUIRED,
env.NODE_ENV === "production",
);
const serviceSlug = textValue(env.NODEDC_DEVICE_MANAGER_SERVICE_SLUG, "device-core");
const launcherBaseUrl = baseUrl(env.NODEDC_LAUNCHER_BASE_URL, "http://127.0.0.1:5173");
const launcherInternalUrl = baseUrl(env.NODEDC_LAUNCHER_INTERNAL_URL, launcherBaseUrl);
const internalToken = textValue(
env.NODEDC_INTERNAL_ACCESS_TOKEN || env.NODEDC_PLATFORM_SERVICE_TOKEN,
"",
);
const sessionCookie = textValue(
env.NODEDC_DEVICE_MANAGER_SESSION_COOKIE,
"nodedc_device_manager_session",
);
const sessionTtlMs = boundedInteger(
env.NODEDC_DEVICE_MANAGER_SESSION_TTL_MS,
DEFAULT_SESSION_TTL_MS,
60_000,
24 * 60 * 60 * 1000,
);
const validationTtlMs = boundedInteger(
env.NODEDC_DEVICE_MANAGER_SESSION_VALIDATION_TTL_MS,
DEFAULT_VALIDATION_TTL_MS,
15_000,
30_000,
);
const validationGraceMs = boundedInteger(
env.NODEDC_DEVICE_MANAGER_SESSION_VALIDATION_GRACE_MS,
DEFAULT_VALIDATION_GRACE_MS,
0,
60_000,
);
const secureCookie = booleanValue(
env.NODEDC_DEVICE_MANAGER_COOKIE_SECURE,
authRequired,
);
const sessions = new Map();
function buildCookie(value, maxAgeSeconds) {
return [
`${sessionCookie}=${encodeURIComponent(value)}`,
"Path=/",
"HttpOnly",
"SameSite=Lax",
`Max-Age=${Math.max(0, Math.floor(maxAgeSeconds))}`,
...(secureCookie ? ["Secure"] : []),
].join("; ");
}
function createSession(response, handoff) {
pruneSessions();
const id = randomBytes(32).toString("base64url");
const createdAt = now();
sessions.set(id, {
id,
user: handoff.user,
launcherSessionId: handoff.launcherSessionId,
expiresAt: createdAt + sessionTtlMs,
validatedAt: createdAt,
validationInFlight: null,
});
appendCookie(response, buildCookie(id, sessionTtlMs / 1000));
}
function currentSession(request) {
const id = parseCookies(request.headers.cookie)[sessionCookie];
const session = id ? sessions.get(id) : null;
if (!session || session.expiresAt <= now()) {
if (id) sessions.delete(id);
return null;
}
return session;
}
async function launcherRequest(pathname, payload) {
if (!internalToken) throw serviceError("device_manager_auth_not_configured", 503);
const response = await fetchImpl(new URL(pathname, launcherInternalUrl), {
method: "POST",
headers: {
Authorization: `Bearer ${internalToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(8_000),
});
const body = await response.json().catch(() => null);
return { response, body };
}
async function handleHandoff(request, response, url) {
const nextPath = safeReturnTo(
url.searchParams.get("next_path") || url.searchParams.get("returnTo"),
);
if (!authRequired) return redirect(response, nextPath);
const token = String(url.searchParams.get("token") || "");
if (!token) return sendText(response, 400, "Missing Launcher handoff token.");
try {
const result = await launcherRequest("/api/internal/handoff/consume", {
token,
serviceSlug,
});
if (!result.response.ok || result.body?.ok !== true || !result.body?.user) {
return sendText(response, 401, "Launcher handoff rejected.");
}
createSession(response, {
user: result.body.user,
launcherSessionId: result.body.launcherSessionId ?? null,
});
return redirect(response, nextPath);
} catch {
return sendText(response, 401, "Launcher handoff failed.");
}
}
async function validatedSession(request, response) {
if (!authRequired) {
return attachSession(request, {
user: {
id: "local-device-admin",
email: "local-device-admin@nodedc.local",
name: "Local Device Admin",
avatarUrl: null,
groups: ["nodedc:superadmin"],
},
});
}
const session = currentSession(request);
if (!session) {
clearCookie(response);
return null;
}
if (now() - session.validatedAt <= validationTtlMs) {
return attachSession(request, session);
}
if (!session.validationInFlight) {
session.validationInFlight = launcherRequest("/api/internal/session/validate", {
serviceSlug,
launcherSessionId: session.launcherSessionId,
}).finally(() => {
session.validationInFlight = null;
});
}
try {
const { response: upstream, body } = await session.validationInFlight;
if (upstream.ok && body?.ok === true && body.active === true) {
session.user = body.user || session.user;
session.validatedAt = now();
return attachSession(request, session);
}
if (upstream.ok && body?.ok === true && body.active === false) {
sessions.delete(session.id);
clearCookie(response);
return null;
}
} catch {
// Read-only grace is resolved below; mutations always fail closed.
}
const readOnly = request.method === "GET" || request.method === "HEAD";
if (readOnly && now() - session.validatedAt <= validationTtlMs + validationGraceMs) {
return attachSession(request, session);
}
request.nodedcDeviceManagerAuthUnavailable = true;
return null;
}
async function authorize(request, response, url) {
if (
url.pathname === "/healthz"
|| url.pathname === "/auth/nodedc/handoff"
|| url.pathname === "/auth/logout"
) return false;
const session = await validatedSession(request, response);
if (!session) {
if (request.nodedcDeviceManagerAuthUnavailable) {
sendJson(response, 503, { ok: false, error: "device_manager_auth_unavailable" });
return true;
}
const loginUrl = new URL("/auth/login", launcherBaseUrl);
const launch = new URL(`/api/services/${encodeURIComponent(serviceSlug)}/launch`, launcherBaseUrl);
launch.searchParams.set("returnTo", safeReturnTo(`${url.pathname}${url.search}`));
loginUrl.searchParams.set("returnTo", `${launch.pathname}${launch.search}`);
if (isHtmlRequest(request, url)) return redirect(response, loginUrl.toString());
sendJson(response, 401, {
ok: false,
error: "device_manager_auth_required",
loginUrl: loginUrl.toString(),
});
return true;
}
const access = resolveAccess(session.user);
if (!access.allowed) {
sendJson(response, 403, {
ok: false,
error: access.blocked
? "device_manager_access_blocked"
: "device_manager_access_denied",
});
return true;
}
request.nodedcDeviceManagerAccess = access;
return false;
}
function handleLogout(request, response) {
const id = parseCookies(request.headers.cookie)[sessionCookie];
if (id) sessions.delete(id);
clearCookie(response);
redirect(response, "/");
}
function currentContext(request) {
const user = request.nodedcDeviceManagerSession?.user;
const access = request.nodedcDeviceManagerAccess ?? resolveAccess(user);
if (!user || !access.allowed) return null;
const id = cleanOpaque(user.id || user.subject || user.sub);
if (!id) return null;
const email = String(user.email || "").trim().slice(0, 240);
const displayName = String(user.name || user.displayName || email || "NODE.DC")
.trim()
.slice(0, 240);
const avatar = String(user.avatarUrl || user.avatar_url || user.picture || "").trim();
const userRef = `user:${id}`;
const ownerScopes = ["admin", "owner"].includes(access.hubRole)
? [{ scopeKind: "personal", ownerRef: userRef, displayName }]
: [];
return {
user: {
id,
email,
displayName,
avatarUrl: /^https:\/\//i.test(avatar) || avatar.startsWith("/") ? avatar : null,
initials: initials(displayName),
},
actor: {
userRef,
hubRole: access.hubRole,
groupRefs: access.groups.map((group) => `group:${group}`),
ownerScopes,
},
profileUrl: new URL("/profile", launcherBaseUrl).toString(),
};
}
function clearCookie(response) {
appendCookie(response, buildCookie("", 0));
}
function pruneSessions() {
const current = now();
for (const [id, session] of sessions) {
if (session.expiresAt <= current) sessions.delete(id);
}
}
return {
authRequired,
serviceSlug,
authorize,
currentContext,
handleHandoff,
handleLogout,
};
}
function resolveAccess(user) {
if (!user || typeof user !== "object") {
return { allowed: false, blocked: false, hubRole: "viewer", groups: [] };
}
const groups = normalizedGroups(user);
if (groups.includes("nodedc:device-core:blocked")) {
return { allowed: false, blocked: true, hubRole: "viewer", groups };
}
const id = cleanOpaque(user.id || user.subject || user.sub);
if (!id) return { allowed: false, blocked: false, hubRole: "viewer", groups };
const hubRole = id === "user_root" || groups.includes("nodedc:superadmin")
? "owner"
: groups.includes("nodedc:device-core:admin") || groups.includes("nodedc:launcher:admin")
? "admin"
: groups.includes("nodedc:device-core:viewer")
? "viewer"
: "member";
return { allowed: true, blocked: false, hubRole, groups };
}
function normalizedGroups(user) {
const values = [user.groups, user.roles, user.roleKeys, user.permissions];
const groups = [];
for (const value of values) {
const items = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
for (const item of items) {
const raw = typeof item === "string" ? item : item?.name || item?.key || item?.slug;
const normalized = String(raw || "").trim().toLowerCase();
if (/^[a-z0-9][a-z0-9._:-]{1,127}$/.test(normalized)) groups.push(normalized);
}
}
return [...new Set(groups)].sort();
}
function attachSession(request, session) {
request.nodedcDeviceManagerSession = session;
return session;
}
function cleanOpaque(value) {
const normalized = String(value || "").trim();
return /^[A-Za-z0-9][A-Za-z0-9._:-]{2,255}$/.test(normalized)
? normalized
: null;
}
function parseCookies(header = "") {
const values = {};
for (const part of String(header).split(";")) {
const index = part.indexOf("=");
if (index < 1) continue;
const key = part.slice(0, index).trim();
try {
values[key] = decodeURIComponent(part.slice(index + 1).trim());
} catch {
values[key] = part.slice(index + 1).trim();
}
}
return values;
}
function appendCookie(response, value) {
const current = response.getHeader("Set-Cookie");
response.setHeader("Set-Cookie", current ? [current, value].flat() : value);
}
function safeReturnTo(value) {
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//")
? value
: "/";
}
function isHtmlRequest(request, url) {
return request.method === "GET"
&& !url.pathname.startsWith("/api/")
&& (url.pathname === "/" || String(request.headers.accept || "").includes("text/html"));
}
function redirect(response, location) {
response.statusCode = 302;
response.setHeader("Location", location);
response.setHeader("Cache-Control", "no-store");
response.end();
}
function sendJson(response, status, body) {
response.statusCode = status;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(JSON.stringify(body));
}
function sendText(response, status, body) {
response.statusCode = status;
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(body);
}
function initials(value) {
return value.split(/\s+/).filter(Boolean).slice(0, 2)
.map((part) => part[0]).join("").toUpperCase() || "DC";
}
function booleanValue(value, fallback) {
if (value == null || value === "") return fallback;
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
}
function boundedInteger(value, fallback, min, max) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isFinite(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback;
}
function textValue(value, fallback) {
return String(value || fallback).trim();
}
function baseUrl(value, fallback) {
return textValue(value, fallback).replace(/\/$/, "");
}
function serviceError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,127 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createDeviceManagerAuth } from "./device-manager-auth.mjs";
const internalToken = "launcher-internal-token-must-stay-server-side";
const launcherSessionId = "launcher-session-id-must-stay-server-side";
test("Launcher handoff becomes an opaque Device Manager session and trusted actor", async () => {
const calls = [];
const auth = createDeviceManagerAuth({
env: productionEnv(),
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init });
return jsonResponse(200, {
ok: true,
launcherSessionId,
user: {
id: "user_root",
email: "root@example.test",
name: "DC SUDO",
groups: ["nodedc:superadmin", "nodedc:device-core:admin"],
},
});
},
});
const response = mockResponse();
await auth.handleHandoff(
{ method: "GET", headers: {} },
response,
new URL("https://device.example.test/auth/nodedc/handoff?token=handoff-secret&next_path=%2F"),
);
assert.equal(response.statusCode, 302);
assert.equal(calls.length, 1);
assert.equal(calls[0].init.headers.Authorization, `Bearer ${internalToken}`);
assert.deepEqual(JSON.parse(calls[0].init.body), {
token: "handoff-secret",
serviceSlug: "device-core",
});
const cookie = String(response.getHeader("set-cookie")).split(";", 1)[0];
assert.match(cookie, /^nodedc_device_manager_session=[A-Za-z0-9_-]{40,}$/);
assert.equal(cookie.includes("user_root"), false);
assert.equal(cookie.includes(launcherSessionId), false);
const request = { method: "GET", headers: { cookie, accept: "application/json" } };
const authorized = await auth.authorize(
request,
mockResponse(),
new URL("https://device.example.test/api/device-manager/session"),
);
assert.equal(authorized, false);
const context = auth.currentContext(request);
assert.equal(context.actor.userRef, "user:user_root");
assert.equal(context.actor.hubRole, "owner");
assert.deepEqual(context.actor.ownerScopes, [{
scopeKind: "personal",
ownerRef: "user:user_root",
displayName: "DC SUDO",
}]);
assert.deepEqual(context.actor.groupRefs, [
"group:nodedc:device-core:admin",
"group:nodedc:superadmin",
]);
assert.equal(JSON.stringify(context).includes(launcherSessionId), false);
assert.equal(JSON.stringify(context).includes(internalToken), false);
});
test("invalid identity and explicit Device Core block never produce an actor", async () => {
for (const user of [
{ id: "?", groups: ["nodedc:device-core:admin"] },
{ id: "valid-user", groups: ["nodedc:superadmin", "nodedc:device-core:blocked"] },
]) {
const auth = createDeviceManagerAuth({
env: productionEnv(),
fetchImpl: async () => jsonResponse(200, {
ok: true,
launcherSessionId,
user,
}),
});
const handoff = mockResponse();
await auth.handleHandoff(
{ method: "GET", headers: {} },
handoff,
new URL("https://device.example.test/auth/nodedc/handoff?token=handoff-secret"),
);
const cookie = String(handoff.getHeader("set-cookie")).split(";", 1)[0];
const request = { method: "GET", headers: { cookie, accept: "application/json" } };
const response = mockResponse();
assert.equal(await auth.authorize(
request,
response,
new URL("https://device.example.test/api/device-manager/session"),
), true);
assert.equal(response.statusCode, 403);
}
});
function productionEnv() {
return {
NODE_ENV: "production",
NODEDC_DEVICE_MANAGER_AUTH_REQUIRED: "true",
NODEDC_DEVICE_MANAGER_COOKIE_SECURE: "false",
NODEDC_LAUNCHER_BASE_URL: "https://launcher.example.test",
NODEDC_LAUNCHER_INTERNAL_URL: "http://launcher.internal.test",
NODEDC_INTERNAL_ACCESS_TOKEN: internalToken,
};
}
function mockResponse() {
const headers = new Map();
return {
statusCode: 200,
body: "",
setHeader(name, value) { headers.set(String(name).toLowerCase(), value); },
getHeader(name) { return headers.get(String(name).toLowerCase()); },
end(body = "") { this.body = String(body); },
};
}
function jsonResponse(status, body) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
@@ -0,0 +1,244 @@
import { createReadStream } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import { createServer } from "node:http";
import { dirname, extname, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createDeviceManagerAuth } from "./device-manager-auth.mjs";
import {
createDeviceCoreClient,
createLocalPreviewDeviceCore,
} from "./device-core-client.mjs";
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const mutationRoutes = new Map([
["/api/device-manager/owner-scopes:ensure", "owner-scopes:ensure"],
["/api/device-manager/projects:ensure", "projects:ensure"],
["/api/device-manager/collections:ensure", "collections:ensure"],
["/api/device-manager/devices:claim", "devices:claim"],
]);
export function createDeviceManagerServer({
auth,
coreClient,
distRoot = resolve(appRoot, "dist"),
} = {}) {
if (!auth || typeof auth.authorize !== "function") {
throw new TypeError("device_manager_auth_required");
}
if (!coreClient || typeof coreClient.listProjects !== "function") {
throw new TypeError("device_manager_core_client_required");
}
return createServer(async (request, response) => {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("Referrer-Policy", "same-origin");
response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
try {
const url = new URL(
request.url || "/",
`http://${request.headers.host || "127.0.0.1"}`,
);
if (request.method === "GET" && url.pathname === "/healthz") {
return sendJson(response, 200, {
ok: true,
service: "nodedc-device-manager",
authRequired: auth.authRequired,
deviceCoreConfigured: coreClient.configured === true,
});
}
if (request.method === "GET" && url.pathname === "/auth/nodedc/handoff") {
return auth.handleHandoff(request, response, url);
}
if (request.method === "GET" && url.pathname === "/auth/logout") {
return auth.handleLogout(request, response);
}
if (await auth.authorize(request, response, url)) return;
const context = auth.currentContext(request);
if (!context) return sendJson(response, 401, {
ok: false,
error: "device_manager_auth_required",
});
if (request.method === "GET" && url.pathname === "/api/device-manager/session") {
return sendJson(response, 200, { ok: true, session: context });
}
if (request.method === "GET" && url.pathname === "/api/device-manager/projects") {
const projects = await coreClient.listProjects(context.actor);
return sendJson(response, 200, { ok: true, projects });
}
const projectRef = workspaceProjectRef(url.pathname);
if (request.method === "GET" && projectRef) {
const workspace = await coreClient.getWorkspace(context.actor, projectRef);
return sendJson(response, 200, { ok: true, workspace });
}
const command = mutationRoutes.get(url.pathname);
if (request.method === "POST" && command) {
const idempotencyKey = singleHeader(request.headers["idempotency-key"]);
const input = await readJsonBody(request, 64 * 1024);
const execution = await coreClient.execute(
command,
context.actor,
input,
idempotencyKey,
);
response.setHeader("Idempotency-Key", idempotencyKey);
response.setHeader(
"Idempotency-Replayed",
execution.replayed ? "true" : "false",
);
return sendJson(response, 200, { ok: true, ...execution });
}
if (url.pathname.startsWith("/api/")) {
return sendJson(response, 404, { ok: false, error: "device_manager_route_not_found" });
}
return serveStatic(request, response, url, distRoot);
} catch (error) {
const statusCode = normalizeStatus(error?.statusCode);
return sendJson(response, statusCode, {
ok: false,
error: safeError(error),
});
}
});
}
export async function createConfiguredDeviceManagerServer({ env = process.env } = {}) {
const localPreview = booleanValue(env.NODEDC_DEVICE_MANAGER_LOCAL_PREVIEW, false);
const auth = createDeviceManagerAuth({ env });
let coreClient;
if (localPreview) {
if (String(env.NODE_ENV || "").toLowerCase() === "production") {
throw new Error("device_manager_local_preview_forbidden");
}
coreClient = createLocalPreviewDeviceCore();
} else {
const tokenFile = String(env.NODEDC_DEVICE_CORE_TOKEN_FILE || "").trim();
if (!tokenFile) throw new Error("device_core_token_file_required");
const token = (await readFile(tokenFile, "utf8")).trim();
coreClient = createDeviceCoreClient({
baseUrl: env.NODEDC_DEVICE_CORE_INTERNAL_URL,
token,
});
}
return createDeviceManagerServer({ auth, coreClient });
}
async function serveStatic(request, response, url, root) {
const requestedPath = url.pathname === "/" ? "/index.html" : url.pathname;
const candidate = resolve(root, `.${decodeURIComponent(requestedPath)}`);
const normalizedRoot = resolve(root);
if (candidate !== normalizedRoot && !candidate.startsWith(`${normalizedRoot}${sep}`)) {
return sendJson(response, 404, { ok: false, error: "device_manager_asset_not_found" });
}
let filePath = candidate;
let info = await stat(filePath).catch(() => null);
if ((!info || !info.isFile()) && !extname(requestedPath)) {
filePath = resolve(root, "index.html");
info = await stat(filePath).catch(() => null);
}
if (!info?.isFile()) {
return sendJson(response, 404, { ok: false, error: "device_manager_asset_not_found" });
}
response.statusCode = 200;
response.setHeader("Content-Type", contentType(filePath));
response.setHeader(
"Cache-Control",
filePath.endsWith("index.html") ? "no-store" : "private, max-age=300",
);
response.setHeader("Content-Length", info.size);
if (request.method === "HEAD") return response.end();
createReadStream(filePath).pipe(response);
}
function workspaceProjectRef(pathname) {
const match = pathname.match(/^\/api\/device-manager\/projects\/([^/]+)\/workspace$/);
if (!match) return null;
const projectRef = decodeURIComponent(match[1]);
return /^project:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(projectRef)
? projectRef.toLowerCase()
: null;
}
async function readJsonBody(request, maxBytes) {
let size = 0;
const chunks = [];
for await (const chunk of request) {
size += chunk.length;
if (size > maxBytes) throw serviceError("device_manager_request_too_large", 413);
chunks.push(chunk);
}
try {
const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error();
return body;
} catch {
throw serviceError("device_manager_json_invalid", 400);
}
}
function singleHeader(value) {
if (Array.isArray(value) || typeof value !== "string") {
throw serviceError("device_idempotency_key_invalid", 400);
}
const normalized = value.trim();
if (!/^[\x21-\x7e]{8,256}$/.test(normalized)) {
throw serviceError("device_idempotency_key_invalid", 400);
}
return normalized;
}
function sendJson(response, statusCode, body) {
response.statusCode = statusCode;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(JSON.stringify(body));
}
function contentType(pathname) {
return ({
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".ico": "image/x-icon",
})[extname(pathname).toLowerCase()] || "application/octet-stream";
}
function normalizeStatus(value) {
const status = Number(value || 500);
return Number.isInteger(status) && status >= 400 && status < 600 ? status : 500;
}
function safeError(error) {
const value = String(error?.message || "");
return /^(?:device|nodedc)_[a-z0-9._:-]{2,160}$/.test(value)
? value
: "device_manager_internal_error";
}
function booleanValue(value, fallback) {
if (value == null || value === "") return fallback;
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
}
function serviceError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const server = await createConfiguredDeviceManagerServer();
const port = Number.parseInt(process.env.PORT || "3335", 10);
const host = String(process.env.HOST || "127.0.0.1");
server.listen(port, host, () => {
console.log(JSON.stringify({
event: "device_manager_started",
host,
port,
}));
});
}
@@ -0,0 +1,94 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createLocalPreviewDeviceCore } from "./device-core-client.mjs";
import { createDeviceManagerAuth } from "./device-manager-auth.mjs";
import { createDeviceManagerServer } from "./device-manager-server.mjs";
test("Device Manager BFF exposes an empty, mutation-driven project workspace", async (t) => {
const auth = createDeviceManagerAuth({
env: { NODEDC_DEVICE_MANAGER_AUTH_REQUIRED: "false" },
});
const coreClient = createLocalPreviewDeviceCore();
const server = createDeviceManagerServer({ auth, coreClient });
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
t.after(() => new Promise((resolve) => server.close(resolve)));
const address = server.address();
const baseUrl = `http://127.0.0.1:${address.port}`;
const session = await getJson(`${baseUrl}/api/device-manager/session`);
assert.equal(session.session.actor.hubRole, "owner");
assert.equal(session.session.actor.userRef, "user:local-device-admin");
assert.deepEqual((await getJson(`${baseUrl}/api/device-manager/projects`)).projects, []);
await postJson(`${baseUrl}/api/device-manager/owner-scopes:ensure`, {
scopeKind: "personal",
ownerRef: "user:local-device-admin",
displayName: "Local Device Admin",
}, {
"X-NODEDC-Hub-Role": "viewer",
"X-NODEDC-User-Ref": "user:spoofed-browser",
});
const created = await postJson(`${baseUrl}/api/device-manager/projects:ensure`, {
scopeKind: "personal",
ownerRef: "user:local-device-admin",
projectKey: "device-sandbox",
name: "Device sandbox",
description: "Created only through the canonical command path",
});
const projectRef = created.result.project.projectRef;
const projects = (await getJson(`${baseUrl}/api/device-manager/projects`)).projects;
assert.equal(projects.length, 1);
assert.equal(projects[0].projectKey, "device-sandbox");
assert.equal(projects[0].ownerScope.ownerRef, "user:local-device-admin");
await postJson(`${baseUrl}/api/device-manager/collections:ensure`, {
projectRef,
collectionKey: "pilot-devices",
name: "Pilot devices",
description: null,
});
const workspace = await getJson(
`${baseUrl}/api/device-manager/projects/${encodeURIComponent(projectRef)}/workspace`,
);
assert.equal(workspace.workspace.project.projectRef, projectRef);
assert.equal(workspace.workspace.collections[0].collectionKey, "pilot-devices");
assert.deepEqual(workspace.workspace.devices, []);
const missingKey = await fetch(`${baseUrl}/api/device-manager/projects:ensure`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
assert.equal(missingKey.status, 400);
assert.equal((await missingKey.json()).error, "device_idempotency_key_invalid");
});
async function getJson(url) {
const response = await fetch(url, { headers: { accept: "application/json" } });
const body = await response.json();
assert.equal(response.status, 200, JSON.stringify(body));
assert.equal(body.ok, true);
return body;
}
async function postJson(url, body, headers = {}) {
const response = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"idempotency-key": `device-manager-test-${crypto.randomUUID()}`,
...headers,
},
body: JSON.stringify(body),
});
const payload = await response.json();
assert.equal(response.status, 200, JSON.stringify(payload));
assert.equal(payload.ok, true);
return payload;
}