NODEDC_LAUNCHER/server/dev-server.mjs

5447 lines
188 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from "express";
import { createServer as createHttpServer } from "node:http";
import { randomBytes, randomUUID, createHash, timingSafeEqual } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { createAuthentikSyncClient, resolveRequiredGroups } from "./authentik-sync.mjs";
import { createControlPlaneStore } from "./control-plane-store.mjs";
import { createNotificationCoreClient } from "./notification-core-client.mjs";
const serverRoot = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(serverRoot, "..");
const maxStorageJsonBodyBytes = "260mb";
const pendingLoginTtlMs = 10 * 60 * 1000;
const serviceHandoffTtlMs = 60 * 1000;
const sessionTtlMs = 12 * 60 * 60 * 1000;
const oidcStateCookieName = "nodedc_oidc_state";
const maxOidcStateCookieEntries = 8;
const sessionCookieName = "nodedc_session";
const noStoreCacheControl = "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
const publicPoolClientId = "client_public_pool";
loadEnvFiles([
process.env.NODEDC_PLATFORM_ENV,
resolve(projectRoot, ".env"),
resolve(projectRoot, "..", "..", "NODEDC", "platform", "infra", ".env"),
]);
const config = readConfig();
const app = express();
const httpServer = createHttpServer(app);
const controlPlaneStore = createControlPlaneStore({ projectRoot });
const runtimeStorageRoot = resolveRuntimeStorageRoot(projectRoot);
const runtimeUploadsRoot = resolveRuntimeUploadsRoot(projectRoot, runtimeStorageRoot);
const authentikSyncClient = createAuthentikSyncClient({ baseUrl: config.authentikBaseUrl, token: config.authentikApiToken });
const notificationCoreClient = createNotificationCoreClient({ baseUrl: config.notificationCoreBaseUrl, token: config.internalAccessToken });
const pendingLogins = new Map();
const serviceHandoffs = new Map();
const sessions = new Map();
const adminMutationIdempotencyRecords = new Map();
const runtimeEventClients = new Set();
let discoveryCache = null;
let jwksCache = null;
const adminMutationIdempotencyTtlMs = 10 * 60 * 1000;
const assistantGatewayName = "ontology-core";
const assistantGatewayAdminRouteAllowlist = [
{ method: "GET", pattern: /^\/api\/admin\/control-plane$/ },
{ method: "PATCH", pattern: /^\/api\/admin\/users\/[^/?#]+\/profile$/ },
{ method: "PATCH", pattern: /^\/api\/admin\/memberships\/[^/?#]+$/ },
];
app.disable("x-powered-by");
app.use((req, res, next) => {
if (shouldDisableHttpCache(req)) {
lockNoStoreHeaders(res);
}
next();
});
app.use(express.json({ limit: maxStorageJsonBodyBytes }));
app.get("/healthz", (_req, res) => {
res.json({
ok: true,
service: "nodedc-launcher-bff",
oidcConfigured: config.oidcConfigured,
authentikApiConfigured: authentikSyncClient.isConfigured(),
internalAccessApiConfigured: Boolean(config.internalAccessToken),
notificationCoreConfigured: notificationCoreClient.isConfigured(),
});
});
app.get("/api/public/brand", (_req, res) => {
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC public brand" });
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Cache-Control", noStoreCacheControl);
res.json(buildPublicBrandResponse(snapshot.data.settings));
});
app.get("/api/public/login-account-status", (req, res) => {
const email = typeof req.query.email === "string" ? req.query.email : "";
res.setHeader("Access-Control-Allow-Origin", "*");
setNoStore(res);
res.json(controlPlaneStore.getLoginAccountStatus(email));
});
app.post("/api/access-requests", asyncRoute(async (req, res) => {
try {
const password = sanitizeNewPassword(req.body?.password);
if (!authentikSyncClient.isConfigured()) {
res.status(503).json({ error: "Authentik API не настроен. Заявку с паролем сейчас создать нельзя." });
return;
}
const result = await controlPlaneStore.createAccessRequest(req.body);
const provisioning = await authentikSyncClient.provisionUser({
data: result.data,
userId: result.user.id,
password,
});
await controlPlaneStore.markUserAuthentikProvisioned(result.user.id, provisioning, {
sub: "public-access-request",
name: "NODE.DC public request",
email: result.user.email,
});
publishControlPlaneEvent("access-request.created", [result.user.id]);
publishNotificationEvent({
type: "nodedc.access_request.created",
sourceService: "launcher",
actor: { userId: result.user.id, email: result.user.email },
subject: { type: "access_request", id: result.accessRequest.id },
payload: {
accessRequestId: result.accessRequest.id,
userId: result.user.id,
email: result.user.email,
status: result.accessRequest.status,
},
idempotencyKey: `launcher:access-request:${result.accessRequest.id}:created:${result.accessRequest.updatedAt}`,
deliveries: [
rootAdminNotificationDelivery(result.data, {
...buildAccessRequestHubNotification(result.accessRequest),
actionable: true,
actionType: "nodedc.access_request.review",
entityType: "access_request",
entityId: result.accessRequest.id,
}),
].filter(Boolean),
});
res.status(201).json({ accessRequest: result.accessRequest });
} catch (error) {
sendAccessRequestApiError(res, error);
}
}));
app.get("/auth/login", asyncRoute(async (req, res) => {
ensureOidcConfigured();
const discovery = await getOidcDiscovery();
const returnTo = sanitizeReturnTo(req.query.returnTo);
const prompt = sanitizePrompt(req.query.prompt);
const cookieStates = getValidOidcCookieStates(req);
const reusableState = cookieStates.find((cookieState) => {
const pendingLogin = pendingLogins.get(cookieState);
return pendingLogin?.returnTo === returnTo && pendingLogin?.prompt === prompt;
});
if (reusableState) {
const pendingLogin = pendingLogins.get(reusableState);
if (pendingLogin) {
res.redirect(buildOidcAuthorizationUrl(discovery, { state: reusableState, prompt, pendingLogin }).toString());
return;
}
}
const state = randomBase64Url(32);
const nonce = randomBase64Url(32);
const codeVerifier = randomBase64Url(64);
pruneExpiredState();
pendingLogins.set(state, {
codeVerifier,
nonce,
returnTo,
prompt,
expiresAt: Date.now() + pendingLoginTtlMs,
});
setOidcStateCookie(res, [state, ...cookieStates].slice(0, maxOidcStateCookieEntries));
const pendingLogin = pendingLogins.get(state);
const authorizationUrl = buildOidcAuthorizationUrl(discovery, { state, prompt, pendingLogin });
res.redirect(authorizationUrl.toString());
}));
app.get("/auth/callback", asyncRoute(async (req, res) => {
ensureOidcConfigured();
const error = typeof req.query.error === "string" ? req.query.error : null;
if (error) {
throw new Error(`OIDC provider returned error: ${error}`);
}
const code = typeof req.query.code === "string" ? req.query.code : null;
const state = typeof req.query.state === "string" ? req.query.state : null;
const cookieStates = getValidOidcCookieStates(req);
if (!code || !state || !cookieStates.includes(state)) {
res.clearCookie(oidcStateCookieName, clearCookieOptions());
res.redirect("/auth/login?returnTo=/");
return;
}
const pendingLogin = pendingLogins.get(state);
pendingLogins.delete(state);
setOidcStateCookie(res, cookieStates.filter((cookieState) => cookieState !== state));
if (!pendingLogin || pendingLogin.expiresAt < Date.now()) {
throw new Error("OIDC login state expired");
}
const discovery = await getOidcDiscovery();
const tokenSet = await exchangeCodeForTokens(discovery, code, pendingLogin.codeVerifier);
const claims = await verifyIdToken(discovery, tokenSet.id_token, pendingLogin.nonce);
createLauncherSession(res, normalizeUser(claims), {
idToken: tokenSet.id_token,
accessToken: tokenSet.access_token ?? null,
expiresAt: tokenSet.expires_in ? Date.now() + Number(tokenSet.expires_in) * 1000 : null,
});
res.redirect(pendingLogin.returnTo);
}));
app.get("/auth/logged-out", (req, res) => {
const returnTo = sanitizeReturnTo(req.query.returnTo);
res.clearCookie(sessionCookieName, clearCookieOptions());
res.clearCookie(oidcStateCookieName, clearCookieOptions());
setNoStore(res);
res.redirect(buildLoginRedirectUrl(returnTo, { includeReturnTo: returnTo !== "/" }));
});
app.get("/auth/session-sync", (req, res) => {
const allowedOrigins = getSessionSyncAllowedOrigins();
setNoStore(res);
res.setHeader(
"Content-Security-Policy",
`default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; frame-ancestors ${allowedOrigins.join(" ")}`
);
res.type("html").send(renderSessionSyncBridgePage(allowedOrigins));
});
app.get("/logout", (req, res) => {
const session = getCurrentSession(req);
if (session) {
sessions.delete(session.id);
}
res.clearCookie(sessionCookieName, clearCookieOptions());
setNoStore(res);
res.type("html").send(
"<!doctype html><html><head><meta charset='utf-8'></head><body>NODE.DC Launcher session closed.</body></html>"
);
});
app.get("/auth/logout", asyncRoute(async (req, res) => {
const session = getCurrentSession(req);
const returnTo = sanitizeReturnTo(req.query.returnTo);
const globalLogout = req.query.global === "1" || req.query.global === "true";
const taskSessionLogoutPromise = globalLogout ? notifyTaskSessionLogout(session) : Promise.resolve();
if (session) {
sessions.delete(session.id);
}
res.clearCookie(sessionCookieName, clearCookieOptions());
if (!globalLogout || !config.oidcConfigured) {
setNoStore(res);
res.redirect(returnTo);
return;
}
const discovery = await getOidcDiscovery();
const logoutUrl = buildOidcLogoutUrl(discovery, returnTo, session?.tokenSet.idToken);
await taskSessionLogoutPromise;
setNoStore(res);
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString()));
}));
app.get("/api/me", (req, res) => {
const session = getCurrentSession(req);
if (!session) {
res.status(401).json({ authenticated: false, loginUrl: "/auth/login" });
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
rejectInactiveSession(res, session, sessionAccess);
return;
}
const runtimeContext = sessionAccess.runtimeContext;
res.json({
authenticated: true,
user: runtimeContext.user,
groups: runtimeContext.groups,
isSuperAdmin: runtimeContext.groups.includes("nodedc:superadmin"),
logoutUrl: "/auth/logout?global=1&returnTo=/",
});
});
app.get("/api/notifications", requireSession, asyncRoute(async (req, res) => {
if (!notificationCoreClient.isConfigured()) {
res.status(503).json({ ok: false, error: "notification_core_not_configured" });
return;
}
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const user = findLauncherUser(data, actor.id);
const payload = await notificationCoreClient.listNotifications({
subject: notificationSubjectFromUser(user),
surface: sanitizeNotificationSurface(req.query.surface, "hub"),
limit: req.query.limit,
unreadOnly: req.query.unreadOnly === "1" || req.query.unreadOnly === "true",
});
res.json(payload);
}));
app.post("/api/notifications/:deliveryId/read", requireSession, asyncRoute(async (req, res) => {
if (!notificationCoreClient.isConfigured()) {
res.status(503).json({ ok: false, error: "notification_core_not_configured" });
return;
}
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const user = findLauncherUser(data, actor.id);
const payload = await notificationCoreClient.markNotificationRead({
subject: notificationSubjectFromUser(user),
deliveryId: req.params.deliveryId,
});
res.json(payload);
}));
app.post("/api/notifications/read-all", requireSession, asyncRoute(async (req, res) => {
if (!notificationCoreClient.isConfigured()) {
res.status(503).json({ ok: false, error: "notification_core_not_configured" });
return;
}
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const user = findLauncherUser(data, actor.id);
const payload = await notificationCoreClient.markAllNotificationsRead({
subject: notificationSubjectFromUser(user),
surface: sanitizeNotificationSurface(req.query.surface ?? req.body?.surface, "hub"),
});
res.json(payload);
}));
app.get("/api/notifications/stream", requireSession, asyncRoute(async (req, res) => {
if (!notificationCoreClient.isConfigured()) {
res.status(503).json({ ok: false, error: "notification_core_not_configured" });
return;
}
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const user = findLauncherUser(data, actor.id);
const abortController = new AbortController();
req.on("close", () => {
abortController.abort();
});
const upstream = await notificationCoreClient.openNotificationStream({
subject: notificationSubjectFromUser(user),
surface: sanitizeNotificationSurface(req.query.surface, "hub"),
signal: abortController.signal,
});
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
res.flushHeaders?.();
const reader = upstream.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(Buffer.from(value));
}
} finally {
reader.releaseLock();
res.end();
}
}));
app.get("/api/apps", (req, res) => {
const session = getCurrentSession(req);
if (!session) {
res.status(401).json({ authenticated: false, loginUrl: "/auth/login" });
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
rejectInactiveSession(res, session, sessionAccess);
return;
}
res.json({ apps: getAppsForUser(sessionAccess.runtimeContext.groups) });
});
app.get("/api/services/:serviceSlug/optional-launch", (req, res) => {
const serviceSlug = sanitizeServiceSlug(req.params.serviceSlug);
const fallbackPath = req.query.fallbackTo || req.query.returnTo || "/";
const fallbackUrl = buildOptionalServiceFallbackUrl(serviceSlug, null, fallbackPath);
const session = getCurrentSession(req);
if (!session) {
setNoStore(res);
res.redirect(fallbackUrl);
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
expireSession(res, session);
setNoStore(res);
res.redirect(fallbackUrl);
return;
}
const runtimeContext = sessionAccess.runtimeContext;
const app = getAppsForUser(runtimeContext.groups).find((candidate) => candidate.slug === serviceSlug);
const appFallbackUrl = buildOptionalServiceFallbackUrl(serviceSlug, app, fallbackPath);
if (!app || !app.hasAccess || app.status !== "active") {
setNoStore(res);
res.redirect(appFallbackUrl);
return;
}
redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/", session);
});
app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => {
const serviceSlug = sanitizeServiceSlug(req.params.serviceSlug);
const runtimeContext = getRuntimeSessionContext(req.nodedcSession);
const app = getAppsForUser(runtimeContext.groups).find((candidate) => candidate.slug === serviceSlug);
if (!app || !app.hasAccess || app.status !== "active") {
res.status(403).type("text/plain").send("NODE.DC service access denied.");
return;
}
redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/", req.nodedcSession);
});
app.post("/api/internal/handoff/consume", (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_handoff_unauthorized" : "internal_handoff_not_configured",
});
return;
}
const token = typeof req.body?.token === "string" ? req.body.token : "";
const serviceSlug = sanitizeServiceSlug(req.body?.serviceSlug);
const handoff = consumeServiceHandoff(token, serviceSlug);
if (!handoff) {
res.status(404).json({ ok: false, error: "handoff_not_found" });
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC handoff validation" });
const user = findInternalAccessUser(snapshot.data, {
subject: handoff.user.sub,
email: handoff.user.email,
});
const groups = user ? resolveRequiredGroups(snapshot.data, user) : handoff.user.groups;
const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
if (!user || !app?.hasAccess) {
res.status(403).json({ ok: false, error: "handoff_access_denied" });
return;
}
res.json({
ok: true,
serviceSlug,
launcherSessionId: handoff.launcherSessionId ?? null,
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: resolveUserAvatarPublicUrl(user),
subject: user.authentikUserId || handoff.user.sub,
authentikUserId: user.authentikUserId ?? null,
groups,
},
});
});
app.post("/api/internal/session/validate", (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
active: false,
error: config.internalAccessToken ? "internal_session_unauthorized" : "internal_session_not_configured",
});
return;
}
const serviceSlug = sanitizeServiceSlug(req.body?.serviceSlug);
const launcherSessionId = typeof req.body?.launcherSessionId === "string" ? req.body.launcherSessionId : "";
if (!launcherSessionId) {
res.json({ ok: true, active: false, error: "launcher_session_missing" });
return;
}
const session = sessions.get(launcherSessionId);
if (!session || session.expiresAt < Date.now()) {
if (session) sessions.delete(session.id);
res.json({ ok: true, active: false, error: "launcher_session_not_found" });
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
sessions.delete(session.id);
res.json({ ok: true, active: false, error: sessionAccess.error || "launcher_session_inactive" });
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC session validation" });
const runtimeContext = sessionAccess.runtimeContext;
const user = findInternalAccessUser(snapshot.data, {
subject: runtimeContext.user.sub,
email: runtimeContext.user.email,
});
const groups = user ? resolveRequiredGroups(snapshot.data, user) : runtimeContext.groups;
const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
if (!user || !app?.hasAccess || app.status !== "active") {
res.json({ ok: true, active: false, error: "service_access_denied" });
return;
}
res.json({
ok: true,
active: true,
serviceSlug,
launcherSessionId,
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: resolveUserAvatarPublicUrl(user),
subject: user.authentikUserId || runtimeContext.user.sub,
authentikUserId: user.authentikUserId ?? null,
groups,
},
});
});
app.get("/api/profile", requireSession, (req, res) => {
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const user = findLauncherUser(data, actor.id);
res.json({
user,
memberships: data.memberships.filter((membership) => membership.userId === user.id),
});
});
app.post("/api/assistant/actions", requireSession, asyncRoute(async (req, res) => {
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const runtimeContext = getRuntimeSessionContext(req.nodedcSession);
const command = buildAssistantActionProxyCommand(req.body, {
actor,
data,
groups: runtimeContext.groups,
});
const payload = await requestAiWorkspaceAssistantJson("/api/ai-workspace/assistant/v1/actions", {
body: command,
actor,
groups: runtimeContext.groups,
role: command.input.launcherGlobalRole,
});
res.json(payload);
}));
app.get("/api/events", requireSession, (req, res) => {
const client = {
id: randomUUID(),
res,
};
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
res.flushHeaders?.();
res.write(`event: nodedc-ready\ndata: ${JSON.stringify({ ok: true })}\n\n`);
const keepAlive = setInterval(() => {
res.write(": keep-alive\n\n");
}, 30000);
runtimeEventClients.add(client);
req.on("close", () => {
clearInterval(keepAlive);
runtimeEventClients.delete(client);
});
});
app.post("/api/internal/access/check", (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC internal access check" });
const user = findInternalAccessUser(snapshot.data, req.body);
const serviceSlug = sanitizeServiceSlug(req.body?.serviceSlug);
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug ?? req.body?.workspace?.slug);
if (!user) {
res.json({
ok: true,
allowed: false,
reason: "user_not_found",
serviceSlug,
groups: [],
matchedGroups: [],
user: null,
});
return;
}
const groups = resolveRequiredGroups(snapshot.data, user);
const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
const allowed = Boolean(app?.hasAccess);
const serviceModules =
serviceSlug === "task-manager"
? resolveTaskManagerWorkspaceServiceModules(snapshot.data, user, serviceSlug, workspaceSlug)
: resolveUserServiceModules(snapshot.data, user, serviceSlug, null);
const workspacePolicy =
serviceSlug === "task-manager"
? resolveTaskManagerWorkspacePolicy(snapshot.data, groups, allowed, user, workspaceSlug, serviceModules)
: null;
res.json({
ok: true,
allowed,
reason: allowed ? "access_confirmed" : "access_denied",
serviceSlug,
groups,
matchedGroups: app?.matchedGroups ?? [],
serviceModules,
workspacePolicy,
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl ?? null,
authentikUserId: user.authentikUserId ?? null,
globalStatus: user.globalStatus,
},
});
});
app.post("/api/internal/tasker/invite-requests", asyncRoute(async (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC tasker invite request" });
const inviterPayload = typeof req.body?.inviter === "object" && req.body.inviter !== null ? req.body.inviter : req.body;
const inviter = findInternalAccessUser(snapshot.data, {
subject: inviterPayload.subject,
email: inviterPayload.email,
userId: inviterPayload.userId,
});
if (!inviter) {
res.status(404).json({ ok: false, error: "inviter_not_found" });
return;
}
const groups = resolveRequiredGroups(snapshot.data, inviter);
const app = getAppsForUser(groups).find((candidate) => candidate.slug === "task-manager");
const workspaceSlug = req.body?.workspace?.slug ?? req.body?.workspaceSlug;
const workspacePolicy = resolveTaskManagerWorkspacePolicy(
snapshot.data,
groups,
Boolean(app?.hasAccess),
inviter,
workspaceSlug
);
if (
!app?.hasAccess ||
workspacePolicy.managedBy !== "tasker" ||
!["nodedc", "launcher"].includes(workspacePolicy.inviteApproval)
) {
res.status(403).json({ ok: false, error: "nodedc_tasker_invite_approval_not_allowed", workspacePolicy });
return;
}
const result = await controlPlaneStore.createTaskerInviteRequest({
taskerInviteId: req.body?.taskerInviteId,
workspaceId: req.body?.workspace?.id ?? req.body?.workspaceId,
workspaceSlug: req.body?.workspace?.slug ?? req.body?.workspaceSlug,
workspaceName: req.body?.workspace?.name ?? req.body?.workspaceName,
inviteeEmail: req.body?.invitee?.email ?? req.body?.inviteeEmail,
role: req.body?.invitee?.role ?? req.body?.role,
inviterUserId: inviter.id,
inviterPlaneUserId: inviterPayload.planeUserId,
inviterEmail: inviter.email,
inviterName: inviter.name,
}, inviter);
publishControlPlaneEvent(
"tasker.invite-request.created",
result.affectedUserIds?.length ? result.affectedUserIds : [inviter.id]
);
publishNotificationEvent({
type: result.taskerInviteRequest.status === "approved" ? "tasker.workspace_invite.approved" : "tasker.workspace_invite.requested",
sourceService: "tasker",
actor: { userId: inviter.id, email: inviter.email },
subject: { type: "tasker_invite_request", id: result.taskerInviteRequest.id },
payload: {
requestId: result.taskerInviteRequest.id,
workspaceSlug: result.taskerInviteRequest.workspaceSlug,
workspaceName: result.taskerInviteRequest.workspaceName,
inviteeEmail: result.taskerInviteRequest.inviteeEmail,
status: result.taskerInviteRequest.status,
},
idempotencyKey: `launcher:tasker-invite:${result.taskerInviteRequest.id}:${result.taskerInviteRequest.status}:${result.taskerInviteRequest.updatedAt}`,
deliveries: (
result.taskerInviteRequest.status === "new"
? [
rootAdminNotificationDelivery(result.data, {
...buildTaskerInviteHubNotification(result.taskerInviteRequest),
actionable: true,
actionType: "tasker.workspace_invite.review",
entityType: "tasker_invite_request",
entityId: result.taskerInviteRequest.id,
}),
]
: notificationUsersForTaskerInvite(result.data, result.taskerInviteRequest).map((user) =>
notificationDeliveryForUser(user, "hub", {
...buildTaskerInviteHubNotification(result.taskerInviteRequest),
entityType: "tasker_invite_request",
entityId: result.taskerInviteRequest.id,
})
)
).filter(Boolean),
});
res.json({ ok: true, taskerInviteRequest: result.taskerInviteRequest, autoApproved: Boolean(result.autoApproved) });
}));
app.post("/api/internal/tasker/invite-requests/cancel", asyncRoute(async (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const result = await controlPlaneStore.cancelTaskerInviteRequest(req.body, {
name: req.body?.cancelledBy?.name ?? req.body?.cancelledBy?.email ?? "Operational Core",
email: req.body?.cancelledBy?.email,
source: "tasker",
});
const syncResult = await syncUsersToAuthentik(result.data, result.affectedUserIds ?? [], {
name: req.body?.cancelledBy?.name ?? req.body?.cancelledBy?.email ?? "Operational Core",
email: req.body?.cancelledBy?.email,
source: "tasker",
});
if (result.taskerInviteRequest) {
publishControlPlaneEvent("tasker.invite-request.cancelled", [
result.taskerInviteRequest.inviterUserId,
...syncResult.userIds,
]);
}
res.json({ ok: true, taskerInviteRequest: result.taskerInviteRequest });
}));
app.post("/api/internal/engine/workflow-access-requests", asyncRoute(async (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC Engine workflow access request" });
const requesterPayload = typeof req.body?.requester === "object" && req.body.requester !== null ? req.body.requester : {};
const requester = findInternalAccessUser(snapshot.data, {
subject: requesterPayload.subject,
email: requesterPayload.email,
userId: requesterPayload.userId,
});
const targetEmail = typeof req.body?.targetEmail === "string" ? req.body.targetEmail.trim().toLowerCase() : "";
const targetUser = findInternalAccessUser(snapshot.data, { email: targetEmail });
if (!requester) {
res.status(404).json({ ok: false, error: "requester_not_found" });
return;
}
if (!targetUser || targetUser.globalStatus !== "active") {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
const groups = resolveRequiredGroups(snapshot.data, targetUser);
const app = getAppsForUser(groups).find((candidate) => candidate.slug === "nodedc");
if (app?.hasAccess) {
res.json({
ok: true,
alreadyAllowed: true,
targetUser: {
id: targetUser.id,
email: targetUser.email,
name: targetUser.name,
avatarUrl: targetUser.avatarUrl ?? null,
},
});
return;
}
const result = await controlPlaneStore.createEngineWorkflowAccessRequest({
workflowId: req.body?.workflow?.id ?? req.body?.workflowId,
workflowName: req.body?.workflow?.name ?? req.body?.workflowName,
targetEmail,
role: req.body?.role,
requesterUserId: requester.id,
requesterEmail: requester.email,
requesterName: requester.name,
}, requester);
publishControlPlaneEvent(
"engine.workflow-access-request.created",
result.affectedUserIds?.length ? result.affectedUserIds : [requester.id, targetUser.id]
);
publishNotificationEvent({
type: "engine.workflow_share.access_requested",
sourceService: "engine",
actor: { userId: requester.id, email: requester.email },
subject: { type: "engine_workflow_access_request", id: result.engineWorkflowAccessRequest.id },
payload: {
requestId: result.engineWorkflowAccessRequest.id,
workflowId: result.engineWorkflowAccessRequest.workflowId,
workflowName: result.engineWorkflowAccessRequest.workflowName,
targetUserId: targetUser.id,
targetEmail: targetUser.email,
role: result.engineWorkflowAccessRequest.role,
},
idempotencyKey: `launcher:engine-workflow-request:${result.engineWorkflowAccessRequest.id}:created:${result.engineWorkflowAccessRequest.updatedAt}`,
deliveries: [
rootAdminNotificationDelivery(result.data, {
...buildEngineWorkflowHubNotification(result.engineWorkflowAccessRequest),
actionable: true,
actionType: "engine.workflow_access.review",
entityType: "engine_workflow_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
].filter(Boolean),
});
res.json({ ok: true, engineWorkflowAccessRequest: result.engineWorkflowAccessRequest });
}));
app.post("/api/internal/engine/role-access-requests", asyncRoute(async (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC Engine role access request" });
const requesterPayload = typeof req.body?.requester === "object" && req.body.requester !== null ? req.body.requester : {};
const requester = findInternalAccessUser(snapshot.data, {
subject: requesterPayload.subject,
email: requesterPayload.email,
userId: requesterPayload.userId,
});
if (!requester || requester.globalStatus !== "active") {
res.status(404).json({ ok: false, error: "requester_not_found" });
return;
}
const result = await controlPlaneStore.createEngineRoleAccessRequest({
requestedRole: req.body?.requestedRole,
currentRole: req.body?.currentRole,
requesterUserId: requester.id,
requesterEmail: requester.email,
requesterName: requester.name,
}, requester);
publishControlPlaneEvent(
"engine.role-access-request.created",
result.affectedUserIds?.length ? result.affectedUserIds : [requester.id, "user_root"]
);
publishNotificationEvent({
type: "engine.role_upgrade.requested",
sourceService: "engine",
actor: { userId: requester.id, email: requester.email },
subject: { type: "engine_role_access_request", id: result.engineWorkflowAccessRequest.id },
payload: {
requestId: result.engineWorkflowAccessRequest.id,
targetUserId: requester.id,
targetEmail: requester.email,
requestedServiceRole: result.engineWorkflowAccessRequest.requestedServiceRole,
currentServiceRole: result.engineWorkflowAccessRequest.currentServiceRole,
},
idempotencyKey: `launcher:engine-role-request:${result.engineWorkflowAccessRequest.id}:created:${result.engineWorkflowAccessRequest.updatedAt}`,
deliveries: [
rootAdminNotificationDelivery(result.data, {
...buildEngineRoleHubNotification(result.engineWorkflowAccessRequest),
actionable: true,
actionType: "engine.role_upgrade.review",
entityType: "engine_role_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
].filter(Boolean),
});
res.json({ ok: true, engineWorkflowAccessRequest: result.engineWorkflowAccessRequest });
}));
app.post("/api/internal/engine/role-access-requests/list", asyncRoute(async (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC Engine role access request list" });
const requesterPayload = typeof req.body?.requester === "object" && req.body.requester !== null ? req.body.requester : {};
const requester = findInternalAccessUser(snapshot.data, {
subject: requesterPayload.subject,
email: requesterPayload.email,
userId: requesterPayload.userId,
});
if (!requester || requester.globalStatus !== "active") {
res.status(404).json({ ok: false, error: "requester_not_found" });
return;
}
const requesterEmail = String(requester.email || "").toLowerCase();
const requesterGroups = Array.isArray(requesterPayload.groups)
? requesterPayload.groups.map((group) => String(group || "").trim()).filter(Boolean)
: [];
const canReviewEngineRoleRequests = requester.id === "user_root" || isLauncherAdmin(requesterGroups);
const requests = snapshot.data.engineWorkflowAccessRequests
.filter((request) => {
if (request.requestType !== "service_role" || request.status === "cancelled") return false;
const isRelated =
request.requesterUserId === requester.id ||
request.targetUserId === requester.id ||
String(request.requesterEmail || "").toLowerCase() === requesterEmail ||
String(request.targetEmail || "").toLowerCase() === requesterEmail;
if (canReviewEngineRoleRequests && request.status === "new") return true;
return isRelated && request.status !== "new";
})
.sort((left, right) => String(right.updatedAt || right.createdAt).localeCompare(String(left.updatedAt || left.createdAt)));
res.json({ ok: true, requests });
}));
app.post("/api/internal/tasker/profile-sync", asyncRoute(async (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
error: config.internalAccessToken ? "internal_access_unauthorized" : "internal_access_not_configured",
});
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC tasker profile sync" });
const user = findInternalAccessUser(snapshot.data, req.body);
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
const patch = sanitizeTaskerProfilePatch(req.body);
if (Object.keys(patch).length === 0) {
res.json({ ok: true, user, data: snapshot.data, skipped: true });
return;
}
const actor = {
sub: "tasker-profile-sync",
name: req.body?.source === "tasker" ? "Operational Core" : "NODE.DC profile sync",
email: typeof req.body?.email === "string" ? req.body.email : user.email,
source: "tasker",
};
const result = await controlPlaneStore.updateUserProfile(user.id, patch, actor);
const syncResult = await syncUsersToAuthentik(result.data, [user.id], actor);
const updatedUser = syncResult.data.users.find((candidate) => candidate.id === user.id) ?? result.user;
publishControlPlaneEvent("tasker.profile.updated", [user.id]);
res.json({ ok: true, user: updatedUser, data: syncResult.data });
}));
app.patch("/api/profile", requireSession, asyncRoute(async (req, res) => {
const { actor } = getLauncherProfileContext(req.nodedcSession);
const result = await controlPlaneStore.updateUserProfile(actor.id, sanitizeSelfProfilePatch(req.body), req.nodedcSession.user);
const provisionedUser = await authentikSyncClient.provisionUser({
data: result.data,
userId: actor.id,
});
const storeResult = await controlPlaneStore.markUserAuthentikProvisioned(actor.id, provisionedUser, req.nodedcSession.user);
const taskManagerProfile = await syncTaskManagerUserProfile(storeResult.user);
publishControlPlaneEvent("profile.updated", [actor.id]);
res.json({
...storeResult,
data: scopeSessionControlPlaneData(req, storeResult.data, actor.id),
provisioning: toProvisioningResponse(provisionedUser),
taskManagerProfile,
});
}));
app.post("/api/profile/password", requireSession, asyncRoute(async (req, res) => {
const newPassword = sanitizeNewPassword(req.body?.newPassword);
const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const provisionedUser = await authentikSyncClient.provisionUser({
data,
userId: actor.id,
password: newPassword,
});
const result = await controlPlaneStore.markUserAuthentikProvisioned(actor.id, provisionedUser, req.nodedcSession.user);
publishControlPlaneEvent("profile.password.updated", [actor.id]);
res.json({ data: scopeSessionControlPlaneData(req, result.data, actor.id), ok: true });
}));
app.get("/api/invites/:token", (req, res) => {
try {
res.json(controlPlaneStore.getInviteByToken(req.params.token));
} catch (error) {
sendInviteApiError(res, error);
}
});
app.post("/api/invites/:token/register", asyncRoute(async (req, res) => {
let payload;
try {
payload = sanitizeInviteRegistrationPayload(req.body);
} catch (error) {
sendInviteApiError(res, error);
return;
}
if (!authentikSyncClient.isConfigured()) {
res.status(503).json({ error: "Регистрация временно недоступна: Authentik API не настроен" });
return;
}
let draft;
try {
draft = controlPlaneStore.prepareInviteRegistration(req.params.token, payload);
} catch (error) {
sendInviteApiError(res, error);
return;
}
const provisionedUser = await authentikSyncClient.provisionUser({
data: draft.data,
userId: draft.user.id,
password: payload.password,
});
const result = await controlPlaneStore.commitInviteRegistration(req.params.token, payload, provisionedUser);
const storeResult = await controlPlaneStore.markUserAuthentikProvisioned(result.user.id, provisionedUser, {
sub: provisionedUser.authentikUserId,
email: result.user.email,
name: result.user.name,
});
const groups = resolveRequiredGroups(storeResult.data, storeResult.user);
createLauncherSession(
res,
normalizeControlPlaneSessionUser(storeResult.user, groups),
{
idToken: null,
accessToken: null,
expiresAt: null,
}
);
publishControlPlaneEvent("invite.registered", [result.user.id]);
const redirectUrl = resolveInviteRedirectUrl(result.invite);
res.json({
...result,
user: storeResult.user,
data: storeResult.data,
provisioning: toProvisioningResponse(provisionedUser),
loginUrl: buildLoginRedirectUrl(redirectUrl, { forceLogin: true, includeReturnTo: true }),
redirectUrl,
authenticated: true,
});
}));
app.post("/api/invites/:token/accept", requireSession, asyncRoute(async (req, res) => {
let result;
try {
result = await controlPlaneStore.acceptInvite(req.params.token, req.nodedcSession.user);
} catch (error) {
sendInviteApiError(res, error);
return;
}
const syncResult = await syncUsersToAuthentik(result.data, [result.user.id], req.nodedcSession.user);
publishControlPlaneEvent("invite.accepted", syncResult.userIds);
res.json({ ...result, data: syncResult.data, redirectUrl: resolveInviteRedirectUrl(result.invite) });
}));
app.get("/tasker-workspace-invite/:taskerInviteRequestId", (req, res) => {
const session = getCurrentSession(req);
if (!session) {
res.redirect(buildLoginRedirectUrl(req.originalUrl, { forceLogin: true }));
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
expireSession(res, session);
res.redirect(buildLoginRedirectUrl(req.originalUrl, { forceLogin: true }));
return;
}
const runtimeContext = sessionAccess.runtimeContext;
const request = controlPlaneStore
.getSnapshot({ name: "NODE.DC tasker invite redirect" })
.data.taskerInviteRequests.find((candidate) => candidate.id === req.params.taskerInviteRequestId);
if (!request || request.status !== "approved") {
res.status(404).send("Workspace-инвайт не найден или ещё не подтверждён NODE.DC.");
return;
}
if (session.user.email?.toLowerCase() !== request.inviteeEmail.toLowerCase()) {
res.status(403).send("Этот workspace-инвайт выписан на другую почту.");
return;
}
const handoffToken = createServiceHandoff("task-manager", runtimeContext.user, { launcherSessionId: session.id });
const taskBaseUrl = getTaskBaseUrl();
const targetUrl = new URL("/auth/nodedc/handoff/", taskBaseUrl);
targetUrl.searchParams.set("token", handoffToken);
targetUrl.searchParams.set(
"next_path",
`/auth/nodedc/workspace-invite/accept/${encodeURIComponent(request.id)}/`
);
res.redirect(targetUrl.toString());
});
app.get("/api/admin/control-plane", requireLauncherAdmin, (req, res) => {
res.json(scopeAdminSnapshot(req));
});
app.get("/api/storage/data", requireSession, (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
if (snapshot.actor.source !== "launcher") {
res.status(403).json({ error: "Профиль пользователя не найден в Launcher control-plane" });
return;
}
res.json(scopeSessionControlPlaneData(req, snapshot.data, snapshot.actor.id));
});
app.patch("/api/admin/settings", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.updateSettings(req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.settings.updated");
res.json(result);
}));
app.get("/api/admin/clients", requireLauncherAdmin, (req, res) => {
const snapshot = scopeAdminSnapshot(req);
res.json({ clients: snapshot.data.clients });
});
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (req, res) => {
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
if (req.nodedcAdminScope?.isRoot) {
res.json(taskManager);
return;
}
const allowedWorkspaceSlugs = new Set(
req.nodedcAdminScope.snapshot.data.clients
.filter((client) => req.nodedcAdminScope.clientIds.has(client.id))
.flatMap((client) => {
const workspaces = Array.isArray(client.integrations?.taskManager?.workspaces)
? client.integrations.taskManager.workspaces
: [];
const slugs = workspaces.map((workspace) => workspace?.slug).filter((slug) => typeof slug === "string" && slug.trim());
const legacySlug = client.integrations?.taskManager?.workspaceSlug;
return legacySlug ? [...slugs, legacySlug] : slugs;
})
);
res.json({
...taskManager,
workspaces: (taskManager.workspaces ?? []).filter((workspace) => allowedWorkspaceSlugs.has(workspace.slug)),
});
}));
app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
if (!client) {
res.status(404).json({ ok: false, error: "client_not_found" });
return;
}
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
const membership = snapshot.data.memberships.find((candidate) => candidate.clientId === client.id && candidate.userId === user.id);
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug) ?? client.integrations?.taskManager?.workspaceSlug ?? null;
const workspaceManagedBy = resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug);
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
const requestedRole = normalizeTaskManagerRole(req.body?.role);
if (requestedRole === "admin") {
res.status(403).json({
ok: false,
error: "task_manager_admin_role_locked",
message: "Admin-роль Operational Core назначается только системно.",
});
return;
}
const role = requestedRole ?? resolveTaskManagerRoleForMembership(membership?.role);
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-memberships/ensure/", {
method: "POST",
body: {
workspaceSlug,
email: user.email,
subject: user.authentikUserId ?? undefined,
avatarUrl: resolveUserAvatarPublicUrl(user),
role,
companyRole: membership?.role ?? null,
managedBy: workspaceManagedBy,
setLastWorkspace: req.body?.setLastWorkspace !== false,
},
});
const result = await controlPlaneStore.recordTaskManagerWorkspaceMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
role,
managedBy: workspaceManagedBy,
taskManager,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
if (!client) {
res.status(404).json({ ok: false, error: "client_not_found" });
return;
}
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
const membership = snapshot.data.memberships.find((candidate) => candidate.clientId === client.id && candidate.userId === user.id);
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug) ?? client.integrations?.taskManager?.workspaceSlug ?? null;
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
if (membership?.role === "client_owner") {
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-memberships/ensure/", {
method: "POST",
body: {
workspaceSlug,
email: user.email,
subject: user.authentikUserId ?? undefined,
avatarUrl: resolveUserAvatarPublicUrl(user),
role: "admin",
companyRole: membership.role,
managedBy: resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug),
setLastWorkspace: false,
},
});
const result = await controlPlaneStore.recordTaskManagerWorkspaceMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
role: "admin",
managedBy: resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug),
taskManager,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager, protected: true });
return;
}
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-memberships/remove/", {
method: "POST",
body: {
workspaceSlug,
email: user.email,
subject: user.authentikUserId ?? undefined,
},
});
const result = await controlPlaneStore.removeTaskManagerWorkspaceMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
if (!client) {
res.status(404).json({ ok: false, error: "client_not_found" });
return;
}
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
const projectId = normalizeOptionalText(req.body?.projectId);
const role = normalizeTaskManagerRole(req.body?.role);
const workspaceManagedBy = resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug);
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
if (!projectId) {
res.status(400).json({ ok: false, error: "task_manager_project_not_configured" });
return;
}
if (!role) {
res.status(400).json({ ok: false, error: "task_manager_role_invalid" });
return;
}
if (role === "admin") {
res.status(403).json({
ok: false,
error: "task_manager_admin_role_locked",
message: "Admin-роль Operational Core назначается только системно.",
});
return;
}
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/project-memberships/ensure/", {
method: "POST",
body: {
workspaceSlug,
projectId,
email: user.email,
subject: user.authentikUserId ?? undefined,
avatarUrl: resolveUserAvatarPublicUrl(user),
role,
managedBy: workspaceManagedBy,
setLastWorkspace: false,
},
});
const result = await controlPlaneStore.recordTaskManagerProjectMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
projectId,
role,
managedBy: workspaceManagedBy,
taskManager,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.project-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/task-manager/project-memberships/remove", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const clientId = typeof req.body?.clientId === "string" ? req.body.clientId : "";
const userId = typeof req.body?.userId === "string" ? req.body.userId : "";
const client = snapshot.data.clients.find((candidate) => candidate.id === clientId);
const user = snapshot.data.users.find((candidate) => candidate.id === userId);
if (!client) {
res.status(404).json({ ok: false, error: "client_not_found" });
return;
}
if (!user) {
res.status(404).json({ ok: false, error: "user_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
const projectId = normalizeOptionalText(req.body?.projectId);
if (!workspaceSlug) {
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
return;
}
if (!projectId) {
res.status(400).json({ ok: false, error: "task_manager_project_not_configured" });
return;
}
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/project-memberships/remove/", {
method: "POST",
body: {
workspaceSlug,
projectId,
email: user.email,
subject: user.authentikUserId ?? undefined,
},
});
const result = await controlPlaneStore.removeTaskManagerProjectMembership(
{
clientId: client.id,
userId: user.id,
workspaceSlug,
projectId,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.task-manager.project-membership.updated", [user.id]);
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
}));
app.post("/api/admin/clients", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
res.status(201).json(result);
}));
app.patch("/api/admin/clients/:clientId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.updateClient(req.params.clientId, req.body, req.nodedcSession.user);
res.json(result);
}));
app.delete("/api/admin/clients/:clientId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.deleteClient(req.params.clientId, req.nodedcSession.user);
res.json(result);
}));
app.get("/api/admin/users", requireLauncherAdmin, (req, res) => {
const snapshot = scopeAdminSnapshot(req);
res.json({ users: snapshot.data.users, memberships: snapshot.data.memberships });
});
app.post("/api/admin/users", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId)) {
return;
}
const result = await controlPlaneStore.createUser(req.body, req.nodedcSession.user);
let provisioning = null;
if (req.body?.provisionAuth !== false) {
const provisionedUser = await authentikSyncClient.provisionUser({
data: result.data,
userId: result.user.id,
password: sanitizePassword(req.body?.password),
generatePassword: req.body?.generatePassword !== false,
});
const storeResult = await controlPlaneStore.markUserAuthentikProvisioned(result.user.id, provisionedUser, req.nodedcSession.user);
result.data = storeResult.data;
provisioning = toProvisioningResponse(provisionedUser);
}
publishControlPlaneEvent("admin.user.created", [result.user.id]);
res.status(201).json({ ...scopeAdminMutationResult(req, result), provisioning });
}));
app.patch("/api/admin/users/:userId/profile", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageUser(req, res, req.params.userId)) {
return;
}
await sendAdminIdempotentJson(req, res, async () => {
const beforeSnapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const beforeUser = beforeSnapshot.data.users.find((candidate) => candidate.id === req.params.userId) ?? null;
const result = await controlPlaneStore.updateUserProfile(req.params.userId, req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, [req.params.userId], req.nodedcSession.user);
const updatedUser = syncResult.data.users.find((candidate) => candidate.id === req.params.userId);
const taskManagerProfile = await syncTaskManagerUserProfile(updatedUser);
const taskManagerCleanup =
beforeUser?.globalStatus === "active" && updatedUser?.globalStatus === "blocked"
? await cleanupTaskManagerUserAccess(updatedUser, {
source: "launcher-user-blocked",
revokeIdentityLinks: false,
revokeTaskerAccess: true,
})
: null;
publishControlPlaneEvent("admin.user.updated", syncResult.userIds);
return { ...scopeAdminMutationResult(req, { ...result, data: syncResult.data }), taskManagerProfile, taskManagerCleanup };
});
}));
app.delete("/api/admin/users/:userId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const user = snapshot.data.users.find((candidate) => candidate.id === req.params.userId);
if (!user) {
res.status(404).json({ error: "user_not_found" });
return;
}
let authentik = null;
if (authentikSyncClient.isConfigured()) {
authentik = await authentikSyncClient.deleteUser({ data: snapshot.data, userId: req.params.userId });
}
const taskManagerCleanup = await cleanupTaskManagerUserAccess(user, {
source: "launcher-user-hard-delete",
revokeIdentityLinks: true,
revokeTaskerAccess: true,
});
const result = await controlPlaneStore.deleteUser(req.params.userId, req.nodedcSession.user);
publishControlPlaneEvent("admin.user.deleted", [req.params.userId]);
res.json({ ...scopeAdminMutationResult(req, result), authentik, taskManagerCleanup });
}));
app.post("/api/admin/users/:userId/provision-authentik", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageUser(req, res, req.params.userId)) {
return;
}
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const provisionedUser = await authentikSyncClient.provisionUser({
data: snapshot.data,
userId: req.params.userId,
password: sanitizePassword(req.body?.password),
generatePassword: req.body?.generatePassword === true,
});
const result = await controlPlaneStore.markUserAuthentikProvisioned(req.params.userId, provisionedUser, req.nodedcSession.user);
publishControlPlaneEvent("admin.user.provisioned", [req.params.userId]);
res.json({ ...scopeAdminMutationResult(req, result), provisioning: toProvisioningResponse(provisionedUser) });
}));
app.patch("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const membership = snapshot.data.memberships.find((candidate) => candidate.id === req.params.membershipId);
if (!membership) {
res.status(404).json({ error: "membership_not_found" });
return;
}
if (!assertAdminCanManageMembership(req, res, membership)) {
return;
}
await sendAdminIdempotentJson(req, res, async () => {
const result = await controlPlaneStore.updateMembership(req.params.membershipId, req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, [result.membership.userId], req.nodedcSession.user);
publishControlPlaneEvent("admin.membership.updated", syncResult.userIds);
return scopeAdminMutationResult(req, { ...result, data: syncResult.data });
});
}));
app.delete("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const membership = snapshot.data.memberships.find((candidate) => candidate.id === req.params.membershipId);
if (!membership) {
res.status(404).json({ error: "membership_not_found" });
return;
}
if (!assertAdminCanManageMembership(req, res, membership)) {
return;
}
const result = await controlPlaneStore.deleteMembership(req.params.membershipId, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, [result.membership.userId], req.nodedcSession.user);
publishControlPlaneEvent("admin.membership.deleted", syncResult.userIds);
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.post("/api/admin/invites", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId)) {
return;
}
const result = await controlPlaneStore.createInvite(req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.invite.created");
res.status(201).json(scopeAdminMutationResult(req, result));
}));
app.patch("/api/admin/invites/:inviteId", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const invite = snapshot.data.invites.find((candidate) => candidate.id === req.params.inviteId);
if (!invite) {
res.status(404).json({ error: "invite_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, invite.clientId)) {
return;
}
const result = await controlPlaneStore.updateInvite(req.params.inviteId, req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.invite.updated");
res.json(scopeAdminMutationResult(req, result));
}));
app.delete("/api/admin/invites/:inviteId", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const invite = snapshot.data.invites.find((candidate) => candidate.id === req.params.inviteId);
if (!invite) {
res.status(404).json({ error: "invite_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, invite.clientId)) {
return;
}
const result = await controlPlaneStore.deleteInvite(req.params.inviteId, req.nodedcSession.user);
publishControlPlaneEvent("admin.invite.deleted");
res.json(scopeAdminMutationResult(req, result));
}));
app.patch("/api/admin/access-requests/:accessRequestId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
try {
const result = await controlPlaneStore.updateAccessRequest(req.params.accessRequestId, req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.access-request.updated");
res.json(scopeAdminMutationResult(req, result));
} catch (error) {
sendAccessRequestApiError(res, error);
}
}));
app.post("/api/admin/access-requests/:accessRequestId/approve", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
try {
let result = await controlPlaneStore.approveAccessRequest(req.params.accessRequestId, req.body, req.nodedcSession.user);
let provisioning = null;
if (result.user && authentikSyncClient.isConfigured()) {
provisioning = await authentikSyncClient.provisionUser({
data: result.data,
userId: result.user.id,
});
const syncResult = await controlPlaneStore.markUserAuthentikProvisioned(result.user.id, provisioning, req.nodedcSession.user);
result = { ...result, data: syncResult.data, user: syncResult.user, provisioning };
}
publishControlPlaneEvent("admin.access-request.approved", result.user ? [result.user.id] : []);
if (result.user) {
publishNotificationEvent({
type: "nodedc.access_request.approved",
sourceService: "launcher",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "access_request", id: result.accessRequest.id },
payload: {
accessRequestId: result.accessRequest.id,
userId: result.user.id,
email: result.user.email,
status: result.accessRequest.status,
},
idempotencyKey: `launcher:access-request:${result.accessRequest.id}:approved:${result.accessRequest.updatedAt}`,
deliveries: [
notificationDeliveryForUser(result.user, "hub", {
...buildAccessRequestHubNotification(result.accessRequest),
entityType: "access_request",
entityId: result.accessRequest.id,
}),
].filter(Boolean),
});
}
res.json(scopeAdminMutationResult(req, result));
} catch (error) {
sendAccessRequestApiError(res, error);
}
}));
app.post("/api/admin/access-requests/:accessRequestId/reject", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
try {
const result = await controlPlaneStore.rejectAccessRequest(req.params.accessRequestId, req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.access-request.rejected");
const targetUser = result.data.users.find((user) => user.email.toLowerCase() === result.accessRequest.email.toLowerCase()) ?? null;
if (targetUser) {
publishNotificationEvent({
type: "nodedc.access_request.rejected",
sourceService: "launcher",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "access_request", id: result.accessRequest.id },
payload: {
accessRequestId: result.accessRequest.id,
userId: targetUser.id,
email: targetUser.email,
status: result.accessRequest.status,
},
idempotencyKey: `launcher:access-request:${result.accessRequest.id}:rejected:${result.accessRequest.updatedAt}`,
deliveries: [
notificationDeliveryForUser(targetUser, "hub", {
...buildAccessRequestHubNotification(result.accessRequest),
entityType: "access_request",
entityId: result.accessRequest.id,
}),
].filter(Boolean),
});
}
res.json(scopeAdminMutationResult(req, result));
} catch (error) {
sendAccessRequestApiError(res, error);
}
}));
app.post("/api/admin/tasker-invite-requests/:taskerInviteRequestId/approve", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const taskerInviteRequest = snapshot.data.taskerInviteRequests.find((request) => request.id === req.params.taskerInviteRequestId);
if (!taskerInviteRequest) {
res.status(404).json({ error: "tasker_invite_request_not_found" });
return;
}
const platformInviteResult = await controlPlaneStore.ensureTaskerInvitePlatformInvite(
req.params.taskerInviteRequestId,
req.nodedcSession.user
);
const platformInviteLink = buildPlatformInviteUrl(platformInviteResult.invite);
const taskerResult = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-invite-requests/approve/", {
body: {
taskerInviteId: taskerInviteRequest.taskerInviteId,
requestId: taskerInviteRequest.id,
platformInviteLink,
},
});
const result = await controlPlaneStore.approveTaskerInviteRequest(
req.params.taskerInviteRequestId,
{
taskerInviteLink: taskerResult.invite?.taskerInviteLink ?? taskerResult.invite?.tasker_invite_link ?? taskerResult.invite?.inviteLink ?? null,
platformInviteId: platformInviteResult.invite.id,
platformInviteToken: platformInviteResult.invite.token,
comment: req.body?.comment,
},
req.nodedcSession.user
);
publishControlPlaneEvent("admin.tasker-invite-request.approved", [result.taskerInviteRequest.inviterUserId]);
publishNotificationEvent({
type: "tasker.workspace_invite.approved",
sourceService: "tasker",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "tasker_invite_request", id: result.taskerInviteRequest.id },
payload: {
requestId: result.taskerInviteRequest.id,
workspaceSlug: result.taskerInviteRequest.workspaceSlug,
workspaceName: result.taskerInviteRequest.workspaceName,
inviteeEmail: result.taskerInviteRequest.inviteeEmail,
status: result.taskerInviteRequest.status,
},
idempotencyKey: `launcher:tasker-invite:${result.taskerInviteRequest.id}:approved:${result.taskerInviteRequest.updatedAt}`,
deliveries: notificationUsersForTaskerInvite(result.data, result.taskerInviteRequest).map((user) =>
notificationDeliveryForUser(user, "hub", {
...buildTaskerInviteHubNotification(result.taskerInviteRequest),
entityType: "tasker_invite_request",
entityId: result.taskerInviteRequest.id,
})
).filter(Boolean),
});
res.json(scopeAdminMutationResult(req, { ...result, tasker: taskerResult }));
}));
app.post("/api/admin/tasker-invite-requests/:taskerInviteRequestId/reject", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const taskerInviteRequest = snapshot.data.taskerInviteRequests.find((request) => request.id === req.params.taskerInviteRequestId);
if (!taskerInviteRequest) {
res.status(404).json({ error: "tasker_invite_request_not_found" });
return;
}
const taskerResult = await requestTaskManagerInternalJson("/api/internal/nodedc/workspace-invite-requests/reject/", {
body: {
taskerInviteId: taskerInviteRequest.taskerInviteId,
requestId: taskerInviteRequest.id,
comment: req.body?.comment,
},
});
const result = await controlPlaneStore.rejectTaskerInviteRequest(req.params.taskerInviteRequestId, req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.tasker-invite-request.rejected", [result.taskerInviteRequest.inviterUserId]);
publishNotificationEvent({
type: "tasker.workspace_invite.rejected",
sourceService: "tasker",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "tasker_invite_request", id: result.taskerInviteRequest.id },
payload: {
requestId: result.taskerInviteRequest.id,
workspaceSlug: result.taskerInviteRequest.workspaceSlug,
workspaceName: result.taskerInviteRequest.workspaceName,
inviteeEmail: result.taskerInviteRequest.inviteeEmail,
status: result.taskerInviteRequest.status,
},
idempotencyKey: `launcher:tasker-invite:${result.taskerInviteRequest.id}:rejected:${result.taskerInviteRequest.updatedAt}`,
deliveries: notificationUsersForTaskerInvite(result.data, result.taskerInviteRequest).map((user) =>
notificationDeliveryForUser(user, "hub", {
...buildTaskerInviteHubNotification(result.taskerInviteRequest),
entityType: "tasker_invite_request",
entityId: result.taskerInviteRequest.id,
})
).filter(Boolean),
});
res.json(scopeAdminMutationResult(req, { ...result, tasker: taskerResult }));
}));
app.post("/api/admin/engine-workflow-access-requests/:engineWorkflowAccessRequestId/approve", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const request = snapshot.data.engineWorkflowAccessRequests.find(
(candidate) => candidate.id === req.params.engineWorkflowAccessRequestId
);
if (!request) {
res.status(404).json({ error: "engine_workflow_access_request_not_found" });
return;
}
if (request.requestType === "service_role") {
const result = await controlPlaneStore.approveEngineWorkflowAccessRequest(
req.params.engineWorkflowAccessRequestId,
{
serviceRole: req.body?.serviceRole ?? req.body?.requestedServiceRole ?? req.body?.role,
comment: req.body?.comment,
},
req.nodedcSession.user
);
const syncResult = await syncUsersToAuthentik(result.data, [result.targetUser.id], req.nodedcSession.user);
publishControlPlaneEvent("admin.engine-role-access-request.approved", [
result.engineWorkflowAccessRequest.requesterUserId,
result.targetUser.id,
]);
publishNotificationEvent({
type: "engine.role_upgrade.approved",
sourceService: "launcher",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "engine_role_access_request", id: result.engineWorkflowAccessRequest.id },
payload: {
requestId: result.engineWorkflowAccessRequest.id,
targetUserId: result.targetUser.id,
targetEmail: result.targetUser.email,
serviceRole: result.engineWorkflowAccessRequest.requestedServiceRole,
},
idempotencyKey: `launcher:engine-role-request:${result.engineWorkflowAccessRequest.id}:approved:${result.engineWorkflowAccessRequest.updatedAt}`,
deliveries: [
notificationDeliveryForUser(result.targetUser, "hub", {
...buildEngineRoleHubNotification(result.engineWorkflowAccessRequest),
entityType: "engine_role_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
notificationDeliveryForUser(result.targetUser, "engine", {
...buildEngineSurfaceNotification(result.engineWorkflowAccessRequest),
entityType: "engine_role_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
].filter(Boolean),
});
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
return;
}
const engineResult = await requestEngineInternalJson(`/api/internal/workflows/${encodeURIComponent(request.workflowId)}/share/users`, {
body: {
email: request.targetEmail,
role: request.role,
requestId: request.id,
approvedBy: {
userId: req.nodedcSession.user?.id,
email: req.nodedcSession.user?.email,
name: req.nodedcSession.user?.name,
},
},
});
const result = await controlPlaneStore.approveEngineWorkflowAccessRequest(
req.params.engineWorkflowAccessRequestId,
{ engineAppliedAt: engineResult.updatedAt ?? new Date().toISOString(), comment: req.body?.comment },
req.nodedcSession.user
);
const syncResult = await syncUsersToAuthentik(result.data, [result.targetUser.id], req.nodedcSession.user);
publishControlPlaneEvent("admin.engine-workflow-access-request.approved", [
result.engineWorkflowAccessRequest.requesterUserId,
result.targetUser.id,
]);
publishNotificationEvent({
type: "engine.workflow_share.approved",
sourceService: "launcher",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "engine_workflow_access_request", id: result.engineWorkflowAccessRequest.id },
payload: {
requestId: result.engineWorkflowAccessRequest.id,
workflowId: result.engineWorkflowAccessRequest.workflowId,
workflowName: result.engineWorkflowAccessRequest.workflowName,
targetUserId: result.targetUser.id,
targetEmail: result.targetUser.email,
role: result.engineWorkflowAccessRequest.role,
},
idempotencyKey: `launcher:engine-workflow-request:${result.engineWorkflowAccessRequest.id}:approved:${result.engineWorkflowAccessRequest.updatedAt}`,
deliveries: [
notificationDeliveryForUser(result.targetUser, "hub", {
...buildEngineWorkflowHubNotification(result.engineWorkflowAccessRequest),
entityType: "engine_workflow_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
notificationDeliveryForUser(result.targetUser, "engine", {
...buildEngineSurfaceNotification(result.engineWorkflowAccessRequest),
entityType: "engine_workflow_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
].filter(Boolean),
});
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data, engine: engineResult }));
}));
app.post("/api/admin/engine-workflow-access-requests/:engineWorkflowAccessRequestId/reject", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.rejectEngineWorkflowAccessRequest(
req.params.engineWorkflowAccessRequestId,
req.body,
req.nodedcSession.user
);
publishControlPlaneEvent("admin.engine-workflow-access-request.rejected", [
result.engineWorkflowAccessRequest.requesterUserId,
]);
const targetUser = result.data.users.find((user) => user.id === result.engineWorkflowAccessRequest.targetUserId) ?? null;
if (targetUser) {
const isServiceRoleRequest = result.engineWorkflowAccessRequest.requestType === "service_role";
publishNotificationEvent({
type: isServiceRoleRequest ? "engine.role_upgrade.rejected" : "engine.workflow_share.rejected",
sourceService: "launcher",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: {
type: isServiceRoleRequest ? "engine_role_access_request" : "engine_workflow_access_request",
id: result.engineWorkflowAccessRequest.id,
},
payload: {
requestId: result.engineWorkflowAccessRequest.id,
targetUserId: targetUser.id,
targetEmail: targetUser.email,
workflowId: result.engineWorkflowAccessRequest.workflowId,
workflowName: result.engineWorkflowAccessRequest.workflowName,
},
idempotencyKey: `launcher:engine-request:${result.engineWorkflowAccessRequest.id}:rejected:${result.engineWorkflowAccessRequest.updatedAt}`,
deliveries: [
notificationDeliveryForUser(targetUser, "hub", {
...(isServiceRoleRequest
? buildEngineRoleHubNotification(result.engineWorkflowAccessRequest)
: buildEngineWorkflowHubNotification(result.engineWorkflowAccessRequest)),
entityType: isServiceRoleRequest ? "engine_role_access_request" : "engine_workflow_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
notificationDeliveryForUser(targetUser, "engine", {
...buildEngineSurfaceNotification(result.engineWorkflowAccessRequest),
entityType: isServiceRoleRequest ? "engine_role_access_request" : "engine_workflow_access_request",
entityId: result.engineWorkflowAccessRequest.id,
}),
].filter(Boolean),
});
}
res.json(scopeAdminMutationResult(req, result));
}));
app.post("/api/admin/groups", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId)) {
return;
}
if (!assertAdminCanManageProtectedGroupMembers(req, res, [], req.body?.memberIds)) {
return;
}
const result = await controlPlaneStore.createGroup(req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user);
publishControlPlaneEvent("admin.group.created", syncResult.userIds);
res.status(201).json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.patch("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (req, res) => {
const beforeSnapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const group = beforeSnapshot.data.groups.find((candidate) => candidate.id === req.params.groupId);
if (!group) {
res.status(404).json({ error: "group_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, group.clientId)) {
return;
}
const previousMemberIds = group.memberIds;
if (!assertAdminCanManageProtectedGroupMembers(req, res, previousMemberIds, req.body?.memberIds)) {
return;
}
const result = await controlPlaneStore.updateGroup(req.params.groupId, req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(
result.data,
[...previousMemberIds, ...result.group.memberIds],
req.nodedcSession.user
);
publishControlPlaneEvent("admin.group.updated", syncResult.userIds);
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.delete("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const group = snapshot.data.groups.find((candidate) => candidate.id === req.params.groupId);
if (!group) {
res.status(404).json({ error: "group_not_found" });
return;
}
if (!assertAdminCanManageClient(req, res, group.clientId)) {
return;
}
if (!assertAdminCanManageProtectedGroupMembers(req, res, group.memberIds, [])) {
return;
}
const result = await controlPlaneStore.deleteGroup(req.params.groupId, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user);
publishControlPlaneEvent("admin.group.deleted", syncResult.userIds);
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.post("/api/admin/services", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.createService(req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.service.created");
res.status(201).json(result);
}));
app.patch("/api/admin/services/reorder", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.reorderServices(req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.service.reordered");
res.json(result);
}));
app.patch("/api/admin/services/:serviceId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.updateService(req.params.serviceId, req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.service.updated");
res.json(result);
}));
app.delete("/api/admin/services/:serviceId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const result = await controlPlaneStore.deleteService(req.params.serviceId, req.nodedcSession.user);
publishControlPlaneEvent("admin.service.deleted");
res.json(result);
}));
app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
if (req.body?.targetType === "user" && isProtectedLauncherUser(req.body?.targetId)) {
res.status(403).json({
ok: false,
error: "protected_user_access_locked",
message: "Доступ системного root-пользователя зафиксирован платформой.",
});
return;
}
if (!assertAdminCanManageGrantTarget(req, res, snapshot.data, req.body?.targetType, req.body?.targetId, { allowProtectedSelf: true })) {
return;
}
const service = snapshot.data.services.find((candidate) => candidate.id === req.body?.serviceId);
if (isTaskManagerService(service) && (req.body?.appRole === "admin" || req.body?.appRole === "owner")) {
res.status(403).json({
ok: false,
error: "task_manager_admin_role_locked",
message: "Admin-роль Operational Core назначается только системно.",
});
return;
}
const result = await controlPlaneStore.upsertGrant(req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(
result.data,
resolveGrantTargetUserIds(result.data, result.grant.targetType, result.grant.targetId),
req.nodedcSession.user
);
publishControlPlaneEvent("admin.access.grant.updated", syncResult.userIds);
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageAccessForUser(req, res, req.body?.userId)) {
return;
}
const result = await controlPlaneStore.upsertException(req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, [result.exception.userId], req.nodedcSession.user);
publishControlPlaneEvent("admin.access.exception.updated", syncResult.userIds);
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageAccessForUser(req, res, req.body?.userId)) {
return;
}
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const service = snapshot.data.services.find((candidate) => candidate.id === req.body?.serviceId);
if (isTaskManagerService(service) && (req.body?.value === "admin" || req.body?.value === "owner")) {
res.status(403).json({
ok: false,
error: "task_manager_admin_role_locked",
message: "Admin-роль Operational Core назначается только системно.",
});
return;
}
if (isPublicTaskManagerGuestServiceAssignment(snapshot.data, req.body)) {
res.status(400).json({
ok: false,
error: "task_manager_public_guest_not_assignable",
message: "Workspace Guest выдаётся только через настройки Operational Core.",
});
return;
}
const result = await controlPlaneStore.setUserServiceAccess(req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, [req.body?.userId], req.nodedcSession.user);
publishControlPlaneEvent(
"admin.access.user-service.updated",
result.affectedUserIds?.length ? result.affectedUserIds : syncResult.userIds
);
if (result.engineWorkflowAccessRequest) {
const targetUser = syncResult.data.users.find((user) => user.id === req.body?.userId) ?? null;
if (targetUser) {
publishNotificationEvent({
type: "engine.role.changed",
sourceService: "launcher",
actor: { userId: req.nodedcSession.user?.id, email: req.nodedcSession.user?.email },
subject: { type: "engine_role_change", id: result.engineWorkflowAccessRequest.id },
payload: {
requestId: result.engineWorkflowAccessRequest.id,
targetUserId: targetUser.id,
targetEmail: targetUser.email,
serviceRole: result.engineWorkflowAccessRequest.requestedServiceRole,
currentServiceRole: result.engineWorkflowAccessRequest.currentServiceRole,
},
idempotencyKey: `launcher:engine-role-change:${result.engineWorkflowAccessRequest.id}`,
deliveries: [
notificationDeliveryForUser(targetUser, "hub", {
...buildEngineRoleHubNotification(result.engineWorkflowAccessRequest),
entityType: "engine_role_change",
entityId: result.engineWorkflowAccessRequest.id,
}),
notificationDeliveryForUser(targetUser, "engine", {
...buildEngineSurfaceNotification(result.engineWorkflowAccessRequest),
entityType: "engine_role_change",
entityId: result.engineWorkflowAccessRequest.id,
}),
].filter(Boolean),
});
}
}
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
}));
app.post("/api/admin/access/service-modules", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId) || !assertAdminCanManageAccessForUser(req, res, req.body?.userId)) {
return;
}
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
const service = snapshot.data.services.find((candidate) => candidate.id === req.body?.serviceId);
if (!service) {
res.status(404).json({ ok: false, error: "service_not_found" });
return;
}
const result = await controlPlaneStore.setServiceModuleEntitlement(req.body, req.nodedcSession.user);
publishControlPlaneEvent("admin.access.service-module.updated", [req.body?.userId]);
res.json(scopeAdminMutationResult(req, result));
}));
app.post("/api/admin/sync/:syncId/retry", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
const retryResult = await controlPlaneStore.retrySync(req.params.syncId, req.nodedcSession.user);
const affectedUserIds = resolveSyncStatusAffectedUserIds(retryResult.data, retryResult.syncStatus);
if (!authentikSyncClient.isConfigured()) {
const result = await controlPlaneStore.updateSyncStatus(
req.params.syncId,
{ state: "error", error: "Authentik API не настроен на сервере launcher." },
req.nodedcSession.user
);
publishControlPlaneEvent("admin.sync.retry.failed");
res.json(scopeAdminMutationResult(req, { ...result, ok: false, message: result.syncStatus.error }));
return;
}
try {
const syncResult = affectedUserIds.length
? await syncUsersToAuthentik(retryResult.data, affectedUserIds, req.nodedcSession.user)
: { data: retryResult.data, userIds: [] };
const result = await controlPlaneStore.updateSyncStatus(
req.params.syncId,
{ state: "synced", error: null },
req.nodedcSession.user
);
publishControlPlaneEvent("admin.sync.retry", syncResult.userIds);
res.json(scopeAdminMutationResult(req, { ...result, ok: true, syncedUserIds: syncResult.userIds }));
} catch (error) {
const message = error instanceof Error ? error.message : "Не удалось выполнить синхронизацию Authentik";
const result = await controlPlaneStore.updateSyncStatus(
req.params.syncId,
{ state: "error", error: message },
req.nodedcSession.user
);
publishControlPlaneEvent("admin.sync.retry.failed");
res.json(scopeAdminMutationResult(req, { ...result, ok: false, message }));
}
}));
app.get("/api/admin/sync/authentik/health", requireLauncherAdmin, requireRootLauncherAdmin, (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
res.json(buildAuthentikSyncHealth(snapshot.data));
});
app.get("/api/admin/sync/authentik/plan", requireLauncherAdmin, requireRootLauncherAdmin, (_req, res) => {
res.json(controlPlaneStore.buildAuthentikSyncPlan());
});
app.post("/api/storage/upload", requireSession, asyncRoute(async (req, res) => {
const result = await saveUploadedFile(req.body);
res.json(result);
}));
app.post("/api/storage/data", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
await saveLauncherData(req.body);
publishControlPlaneEvent("storage.data.updated");
res.json({ ok: true });
}));
app.get("/storage/launcher-data.json", (_req, res) => {
setNoStore(res);
res.status(404).json({ error: "not_found" });
});
for (const uploadRoot of getReadableUploadRoots()) {
app.use(
"/storage/uploads",
express.static(uploadRoot, {
fallthrough: true,
immutable: true,
maxAge: "1y",
setHeaders(res) {
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
},
})
);
}
let fixFrontendStacktrace = () => {};
if (process.env.NODE_ENV === "production") {
const distRoot = join(projectRoot, "dist");
const indexHtmlPath = join(distRoot, "index.html");
if (!existsSync(indexHtmlPath)) {
throw new Error("Launcher production build is missing. Run npm run build before starting the server.");
}
app.use(express.static(distRoot, {
index: false,
immutable: true,
maxAge: "1y",
setHeaders(res, assetPath) {
if (assetPath === indexHtmlPath) {
res.setHeader("Cache-Control", noStoreCacheControl);
return;
}
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
},
}));
app.use((req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") {
next();
return;
}
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
if (!accept.includes("text/html")) {
next();
return;
}
setNoStore(res);
res.sendFile(indexHtmlPath);
});
} else {
const { createServer: createViteServer } = await import("vite");
const vite = await createViteServer({
root: projectRoot,
appType: "spa",
server: {
middlewareMode: true,
hmr: { server: httpServer },
},
});
fixFrontendStacktrace = (error) => vite.ssrFixStacktrace(error);
app.use(vite.middlewares);
}
app.use((error, _req, res, _next) => {
fixFrontendStacktrace(error);
const message = error instanceof Error ? error.message : "Unexpected server error";
res.status(500).json({ error: message });
});
httpServer.listen(config.port, "0.0.0.0", () => {
console.log(`NODE.DC launcher BFF listening on http://0.0.0.0:${config.port}`);
});
function readConfig() {
const issuer = process.env.LAUNCHER_OIDC_ISSUER ?? "";
const clientId = process.env.LAUNCHER_OIDC_CLIENT_ID ?? "";
const clientSecret = process.env.LAUNCHER_OIDC_CLIENT_SECRET ?? "";
const launcherDomain = process.env.LAUNCHER_DOMAIN ?? "localhost:5173";
const appBaseUrl = process.env.LAUNCHER_BASE_URL ?? `http://${launcherDomain}`;
return {
port: Number(process.env.PORT ?? "5173"),
issuer,
clientId,
clientSecret,
redirectUri: process.env.LAUNCHER_OIDC_REDIRECT_URI ?? `${appBaseUrl}/auth/callback`,
appBaseUrl,
scope: process.env.LAUNCHER_OIDC_SCOPE ?? "openid email profile groups offline_access",
cookieDomain: process.env.LAUNCHER_COOKIE_DOMAIN || undefined,
cookieSecure: process.env.COOKIE_SECURE === "true",
oidcConfigured: Boolean(issuer && clientId && clientSecret),
authentikBaseUrl:
process.env.NODEDC_AUTHENTIK_BASE_URL ??
process.env.AUTHENTIK_BASE_URL ??
(process.env.AUTH_DOMAIN ? `http://${process.env.AUTH_DOMAIN}` : ""),
authentikApiToken:
process.env.NODEDC_AUTHENTIK_SERVICE_TOKEN ??
process.env.AUTHENTIK_SERVICE_TOKEN ??
process.env.AUTHENTIK_BOOTSTRAP_TOKEN ??
"",
internalAccessToken:
process.env.NODEDC_INTERNAL_ACCESS_TOKEN ??
process.env.NODEDC_PLATFORM_SERVICE_TOKEN ??
"",
taskLogoutUrl:
process.env.TASK_LOGOUT_URL ??
`${(process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`).replace(/\/$/, "")}/logout`,
taskInternalLogoutUrl:
process.env.TASK_INTERNAL_LOGOUT_URL ??
`${(process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`).replace(/\/$/, "")}/api/internal/nodedc/logout/`,
engineBaseUrl:
process.env.NODEDC_ENGINE_INTERNAL_URL ??
process.env.NODEDC_ENGINE_BASE_URL ??
process.env.ENGINE_BASE_URL ??
"https://engine.nodedc.ru",
notificationCoreBaseUrl:
process.env.NODEDC_NOTIFICATION_CORE_URL ??
process.env.NOTIFICATION_CORE_URL ??
"",
};
}
function ensureOidcConfigured() {
if (!config.oidcConfigured) {
throw new Error("Launcher OIDC is not configured. Set LAUNCHER_OIDC_ISSUER, LAUNCHER_OIDC_CLIENT_ID and LAUNCHER_OIDC_CLIENT_SECRET.");
}
}
async function getOidcDiscovery() {
if (discoveryCache && discoveryCache.expiresAt > Date.now()) {
return discoveryCache.discovery;
}
const discoveryUrl = new URL("./.well-known/openid-configuration", ensureTrailingSlash(config.issuer));
const response = await fetch(discoveryUrl, { headers: { Accept: "application/json" } });
if (!response.ok) {
throw new Error(`Unable to load OIDC discovery from ${discoveryUrl}: HTTP ${response.status}`);
}
const discovery = await response.json();
discoveryCache = { discovery, expiresAt: Date.now() + 5 * 60 * 1000 };
return discovery;
}
function buildOidcAuthorizationUrl(discovery, { state, prompt, pendingLogin }) {
const codeChallenge = createHash("sha256").update(pendingLogin.codeVerifier).digest("base64url");
const authorizationUrl = new URL(discovery.authorization_endpoint);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("client_id", config.clientId);
authorizationUrl.searchParams.set("redirect_uri", config.redirectUri);
authorizationUrl.searchParams.set("scope", config.scope);
authorizationUrl.searchParams.set("state", state);
authorizationUrl.searchParams.set("nonce", pendingLogin.nonce);
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
authorizationUrl.searchParams.set("code_challenge_method", "S256");
if (prompt) {
authorizationUrl.searchParams.set("prompt", prompt);
}
if (prompt === "login") {
authorizationUrl.searchParams.set("max_age", "0");
}
return authorizationUrl;
}
async function exchangeCodeForTokens(discovery, code, codeVerifier) {
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: config.redirectUri,
code_verifier: codeVerifier,
});
const response = await fetch(discovery.token_endpoint, {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OIDC token exchange failed: HTTP ${response.status} ${errorText}`);
}
const tokenSet = await response.json();
if (!tokenSet.id_token) {
throw new Error("OIDC token response does not contain id_token");
}
return tokenSet;
}
async function verifyIdToken(discovery, idToken, nonce) {
if (!jwksCache || jwksCache.uri !== discovery.jwks_uri) {
jwksCache = {
uri: discovery.jwks_uri,
jwks: createRemoteJWKSet(new URL(discovery.jwks_uri)),
};
}
const { payload } = await jwtVerify(idToken, jwksCache.jwks, {
issuer: discovery.issuer ?? config.issuer,
audience: config.clientId,
});
if (payload.nonce !== nonce) {
throw new Error("OIDC nonce validation failed");
}
return payload;
}
function normalizeUser(claims) {
const groups = normalizeGroups(claims.groups);
const email = typeof claims.email === "string" ? claims.email : "";
const avatarUrl = firstStringClaim(claims.picture, claims.avatar_url, claims.avatar);
const name =
typeof claims.name === "string" && claims.name
? claims.name
: typeof claims.preferred_username === "string" && claims.preferred_username
? claims.preferred_username
: email || String(claims.sub);
return {
sub: String(claims.sub),
email,
name,
preferredUsername: typeof claims.preferred_username === "string" ? claims.preferred_username : null,
avatarUrl,
groups,
};
}
function normalizeControlPlaneSessionUser(user, groups) {
return {
sub: String(user.authentikUserId || user.id),
email: user.email,
name: user.name,
preferredUsername: user.email,
avatarUrl: user.avatarUrl ?? null,
groups,
};
}
function createLauncherSession(res, user, tokenSet = {}) {
const sessionId = randomBase64Url(48);
const session = {
id: sessionId,
user,
tokenSet: {
idToken: tokenSet.idToken ?? null,
accessToken: tokenSet.accessToken ?? null,
expiresAt: tokenSet.expiresAt ?? null,
},
createdAt: Date.now(),
expiresAt: Date.now() + sessionTtlMs,
};
pruneExpiredSessions();
sessions.set(sessionId, session);
res.cookie(sessionCookieName, sessionId, cookieOptions(sessionTtlMs));
return session;
}
function firstStringClaim(...values) {
for (const value of values) {
if (typeof value === "string" && value) return value;
}
return null;
}
function sanitizePassword(value) {
return typeof value === "string" && value.length >= 8 ? value : null;
}
function sanitizeNewPassword(value) {
if (typeof value !== "string" || value.length < 8) {
throw new Error("Новый пароль должен быть не короче 8 символов");
}
return value;
}
function sanitizeInviteRegistrationPayload(payload) {
const email = typeof payload?.email === "string" ? payload.email.trim().toLowerCase() : "";
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
if (!isValidInviteRegistrationEmail(email)) {
throw new Error("Введите почту, на которую выписан инвайт");
}
if (!name) {
throw new Error("Введите имя");
}
return {
email,
name: name.slice(0, 120),
password: sanitizeNewPassword(payload?.password),
};
}
function isValidInviteRegistrationEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function sendInviteApiError(res, error) {
const message = error instanceof Error ? error.message : "Инвайт недоступен";
const status =
message.includes("не найден")
? 404
: message.includes("другую почту") || message.includes("нет активного инвайта")
? 403
: message.includes("истёк") || message.includes("отозван")
? 410
: 400;
res.status(status).json({ error: message });
}
function sendAccessRequestApiError(res, error) {
const message = error instanceof Error ? error.message : "Заявка недоступна";
const status =
message.includes("Unknown access_request") || message.includes("не найден")
? 404
: message.includes("нельзя")
? 409
: message.includes("required") || message.includes("Введите")
? 400
: 400;
res.status(status).json({ error: message });
}
function sanitizeSelfProfilePatch(payload) {
return {
name: payload?.name,
email: payload?.email,
phone: payload?.phone,
position: payload?.position,
avatarUrl: payload?.avatarUrl,
};
}
function sanitizeTaskerProfilePatch(payload) {
const patch = {};
const changedFields = Array.isArray(payload?.changedFields) ? payload.changedFields : [];
const taskerFullName = joinTaskerProfileName(
payload?.firstName ?? payload?.first_name,
payload?.lastName ?? payload?.last_name
);
const taskerDisplayName = firstNonEmptyString(payload?.displayName, payload?.display_name, payload?.name);
const name =
hasChangedField(changedFields, ["display_name"]) || !hasChangedField(changedFields, ["first_name", "last_name"])
? firstNonEmptyString(taskerDisplayName, taskerFullName)
: firstNonEmptyString(taskerFullName, taskerDisplayName);
const hasAvatar =
Object.hasOwn(payload ?? {}, "avatarUrl") ||
Object.hasOwn(payload ?? {}, "avatar_url") ||
Object.hasOwn(payload ?? {}, "avatar");
if (name) {
patch.name = name;
}
if (hasAvatar) {
patch.avatarUrl = nullableProfileUrl(payload?.avatarUrl ?? payload?.avatar_url ?? payload?.avatar);
}
return patch;
}
function firstNonEmptyString(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return null;
}
function joinTaskerProfileName(firstName, lastName) {
return [firstName, lastName].filter((value) => typeof value === "string" && value.trim()).join(" ").trim();
}
function splitTaskerProfileName(name) {
const trimmedName = typeof name === "string" ? name.trim() : "";
const parts = trimmedName.split(/\s+/, 2);
return {
firstName: parts[0] ?? "",
lastName: parts.length > 1 ? trimmedName.slice(parts[0].length).trim() : "",
};
}
function hasChangedField(changedFields, fields) {
return fields.some((field) => changedFields.includes(field));
}
function nullableProfileUrl(value) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function toProvisioningResponse(provisionedUser) {
return {
authentikUserId: provisionedUser.authentikUserId,
email: provisionedUser.email,
name: provisionedUser.name,
groups: provisionedUser.groups,
created: provisionedUser.created,
temporaryPassword: provisionedUser.temporaryPassword,
};
}
async function syncUsersToAuthentik(data, userIds, identity) {
let latestData = data;
const uniqueUserIds = [...new Set(userIds.filter((userId) => typeof userId === "string" && userId))];
for (const userId of uniqueUserIds) {
if (!latestData.users.some((user) => user.id === userId)) {
continue;
}
const provisionedUser = await authentikSyncClient.provisionUser({ data: latestData, userId });
const result = await controlPlaneStore.markUserAuthentikProvisioned(userId, provisionedUser, identity);
latestData = result.data;
}
return { data: latestData, userIds: uniqueUserIds };
}
function buildAuthentikSyncHealth(data) {
const rows = data.syncStatuses.filter((syncStatus) => syncStatus.target === "authentik");
const errors = rows.filter((syncStatus) => syncStatus.state === "error");
const pending = rows.filter((syncStatus) => syncStatus.state === "pending");
const freshPending = pending.filter(isFreshPendingSyncStatus);
const stalePending = pending.filter((syncStatus) => !isFreshPendingSyncStatus(syncStatus));
const state = !authentikSyncClient.isConfigured()
? "not_configured"
: errors.length > 0
? "error"
: freshPending.length > 0
? "pending"
: "ok";
return {
configured: authentikSyncClient.isConfigured(),
state,
checkedAt: new Date().toISOString(),
counts: {
total: rows.length,
synced: rows.filter((syncStatus) => syncStatus.state === "synced").length,
pending: pending.length,
freshPending: freshPending.length,
stalePending: stalePending.length,
errors: errors.length,
disabled: rows.filter((syncStatus) => syncStatus.state === "disabled").length,
},
};
}
function isFreshPendingSyncStatus(syncStatus) {
if (syncStatus.state !== "pending") return false;
const timestamp = Date.parse(syncStatus.updatedAt ?? syncStatus.lastSyncAt ?? "");
if (!Number.isFinite(timestamp)) return false;
return Date.now() - timestamp <= 5 * 60 * 1000;
}
function resolveSyncStatusAffectedUserIds(data, syncStatus) {
if (!syncStatus || syncStatus.target !== "authentik") {
return [];
}
if (syncStatus.objectType === "user") {
return data.users.some((user) => user.id === syncStatus.objectId) ? [syncStatus.objectId] : [];
}
if (syncStatus.objectType === "group") {
return data.groups.find((group) => group.id === syncStatus.objectId)?.memberIds ?? [];
}
if (syncStatus.objectType === "client") {
return data.memberships.filter((membership) => membership.clientId === syncStatus.objectId).map((membership) => membership.userId);
}
if (syncStatus.objectType === "service") {
return resolveServiceSyncUserIds(data, syncStatus.objectId);
}
if (syncStatus.objectType === "grant") {
return resolveGrantSyncUserIds(data, syncStatus);
}
if (syncStatus.objectType === "invite") {
const invite = data.invites.find((candidate) => candidate.id === syncStatus.objectId);
return invite ? resolveUserIdsByEmail(data, invite.email) : resolveUserIdsByEmail(data, syncStatus.objectName);
}
return [];
}
function resolveServiceSyncUserIds(data, serviceId) {
const userIds = new Set();
for (const grant of data.grants.filter((candidate) => candidate.serviceId === serviceId && candidate.status === "active")) {
for (const userId of resolveGrantTargetUserIds(data, grant.targetType, grant.targetId)) {
userIds.add(userId);
}
}
for (const exception of data.exceptions.filter((candidate) => candidate.serviceId === serviceId)) {
userIds.add(exception.userId);
}
return [...userIds];
}
function resolveGrantSyncUserIds(data, syncStatus) {
const existingGrant = data.grants.find((grant) => grant.id === syncStatus.objectId);
if (existingGrant) {
return resolveGrantTargetUserIds(data, existingGrant.targetType, existingGrant.targetId);
}
const [serviceId, userId] = String(syncStatus.objectId).split(":");
if (serviceId && userId && data.users.some((user) => user.id === userId)) {
return [userId];
}
return resolveUserIdsByEmail(data, syncStatus.objectName);
}
function resolveUserIdsByEmail(data, value) {
const normalizedValue = String(value ?? "").toLowerCase();
if (!normalizedValue) return [];
return data.users
.filter((user) => normalizedValue.includes(String(user.email ?? "").toLowerCase()))
.map((user) => user.id);
}
function resolveGrantTargetUserIds(data, targetType, targetId) {
if (targetType === "user") {
return [targetId];
}
if (targetType === "group") {
return data.groups.find((group) => group.id === targetId)?.memberIds ?? [];
}
if (targetType === "client") {
return data.memberships.filter((membership) => membership.clientId === targetId).map((membership) => membership.userId);
}
return [];
}
function publishControlPlaneEvent(action, affectedUserIds = []) {
publishRuntimeEvent({
type: "control-plane.updated",
action,
affectedUserIds: [...new Set(affectedUserIds.filter((userId) => typeof userId === "string" && userId))],
emittedAt: new Date().toISOString(),
});
}
function publishRuntimeEvent(payload) {
const message = `event: nodedc-runtime\ndata: ${JSON.stringify(payload)}\n\n`;
for (const client of runtimeEventClients) {
try {
client.res.write(message);
} catch {
runtimeEventClients.delete(client);
}
}
}
function normalizeGroups(groupsClaim) {
if (Array.isArray(groupsClaim)) {
return [...new Set(groupsClaim.filter((group) => typeof group === "string"))];
}
if (typeof groupsClaim === "string" && groupsClaim) {
return [groupsClaim];
}
return [];
}
function getRuntimeSessionContext(session) {
const fallback = {
user: session.user,
groups: session.user.groups,
};
try {
const snapshot = controlPlaneStore.getSnapshot(session.user);
if (snapshot.actor.source !== "launcher") {
return fallback;
}
const user = snapshot.data.users.find((candidate) => candidate.id === snapshot.actor.id);
if (!user) {
return fallback;
}
return buildRuntimeSessionContext(session, snapshot.data, user);
} catch (error) {
console.warn(error instanceof Error ? error.message : "Не удалось рассчитать runtime контекст Launcher");
return fallback;
}
}
function buildRuntimeSessionContext(session, data, user) {
const groups = resolveRequiredGroups(data, user);
return {
groups,
user: {
...session.user,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl ?? session.user.avatarUrl,
groups,
},
};
}
function getSessionAccessState(session) {
try {
const snapshot = controlPlaneStore.getSnapshot(session.user);
const sessionEmail = typeof session.user?.email === "string" ? session.user.email.toLowerCase() : "";
if (snapshot.actor.source !== "launcher") {
const revokedAccount = snapshot.data.revokedAccounts.find((account) => account.email.toLowerCase() === sessionEmail);
return {
ok: false,
status: 401,
error: revokedAccount ? "account_revoked" : "account_not_found",
message: revokedAccount
? "Аккаунт больше не активен. Запросите доступ, если хотите подключиться снова."
: "Профиль пользователя не найден в Launcher control-plane.",
};
}
const user = snapshot.data.users.find((candidate) => candidate.id === snapshot.actor.id);
if (!user) {
return {
ok: false,
status: 401,
error: "account_not_found",
message: "Профиль пользователя не найден в Launcher control-plane.",
};
}
if (user.globalStatus === "blocked") {
return {
ok: false,
status: 401,
error: "account_blocked",
message: "Аккаунт заблокирован. Обратитесь к администратору NODE.DC.",
};
}
return {
ok: true,
runtimeContext: buildRuntimeSessionContext(session, snapshot.data, user),
};
} catch (error) {
console.warn(error instanceof Error ? error.message : "Не удалось проверить Launcher-сессию");
return {
ok: false,
status: 401,
error: "session_invalid",
message: "Не удалось проверить Launcher-сессию.",
};
}
}
function getAppsForUser(userGroups) {
const groupSet = new Set(userGroups);
const catalog = getAppCatalog();
return catalog.map((app) => {
const matchedGroups = app.requiredGroups.filter((group) => groupSet.has(group));
const isSuperAdmin = groupSet.has("nodedc:superadmin");
const isPublic = app.requiredGroups.length === 0;
const hasAccess = isSuperAdmin || isPublic || matchedGroups.length > 0;
return {
...app,
matchedGroups: isSuperAdmin ? ["nodedc:superadmin", ...matchedGroups] : matchedGroups,
hasAccess,
accessReason: hasAccess ? "Доступ подтверждён" : "Нет доступа",
};
});
}
function getAppCatalog() {
const launcherData = readLauncherData();
const services = Array.isArray(launcherData?.services) ? launcherData.services : [];
const serviceCatalog = services.map((service) => {
const specialGroups = specialRequiredGroups(service);
const requiredGroups = specialGroups.length
? specialGroups
: service.authentikGroupName
? [service.authentikGroupName]
: [];
return {
id: service.id,
slug: service.slug,
title: service.title,
description: service.description,
url: getServiceUrl(service),
openUrl: getServiceUrl(service),
launchTargetUrl: getServiceLaunchTargetUrl(service),
status: service.status ?? "disabled",
provider: "authentik",
requiredGroups,
media: {
icon: service.iconUrl ?? null,
coverImage: service.coverImageUrl ?? null,
accentColor: service.accentColor ?? null,
},
};
});
return [
{
id: "launcher",
slug: "launcher",
title: "NODE.DC Launcher",
description: "Единая точка входа в приложения NODE.DC.",
url: config.appBaseUrl,
openUrl: config.appBaseUrl,
status: "active",
provider: "authentik",
requiredGroups: ["nodedc:launcher:admin", "nodedc:launcher:user"],
},
...serviceCatalog.filter((service) => service.slug !== "launcher"),
];
}
const engineRequiredGroups = [
"nodedc_admin",
"nodedc_editor",
"nodedc_viewer",
];
function isEngineServiceSlug(serviceSlug) {
const slug = sanitizeServiceSlug(serviceSlug);
return slug === "nodedc" || slug === "engine" || slug === "nodedc-engine";
}
function isBimViewerServiceSlug(serviceSlug) {
const slug = sanitizeServiceSlug(serviceSlug);
return slug === "bim-viewer" || slug === "beam-viewer" || slug === "bim";
}
function specialRequiredGroups(serviceOrSlug) {
const slug = typeof serviceOrSlug === "string" ? serviceOrSlug : serviceOrSlug?.slug;
const applicationSlug = typeof serviceOrSlug === "string" ? null : serviceOrSlug?.authentikApplicationSlug;
const serviceId = typeof serviceOrSlug === "string" ? null : serviceOrSlug?.id;
if (slug === "launcher") return ["nodedc:launcher:admin", "nodedc:launcher:user"];
if (slug === "task-manager") return ["nodedc:taskmanager:admin", "nodedc:taskmanager:user"];
if (slug === "nodedc" || slug === "engine" || slug === "nodedc-engine" || applicationSlug === "nodedc-engine" || serviceId === "service_nodedc") {
return engineRequiredGroups;
}
return [];
}
function getServiceUrl(service) {
if (service.slug === "task-manager" || isEngineServiceSlug(service.slug) || isBimViewerServiceSlug(service.slug)) {
return `/api/services/${encodeURIComponent(service.slug)}/launch`;
}
return service.launchUrl || service.url || "#";
}
function getServiceLaunchTargetUrl(service) {
if (isEngineServiceSlug(service?.slug)) {
return service.launchUrl || service.url || getEnginePublicBaseUrl();
}
return service?.launchUrl || service?.url || "#";
}
function getTaskBaseUrl() {
const taskBaseUrl = process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`;
return taskBaseUrl.replace(/\/$/, "");
}
function getBimViewerBaseUrl(app = null) {
return String(
process.env.NODEDC_BIM_VIEWER_PUBLIC_URL ||
process.env.BIM_VIEWER_BASE_URL ||
app?.launchTargetUrl ||
app?.openUrl ||
app?.url ||
"https://bim.nodedc.tech"
).replace(/\/$/, "");
}
function findServiceCatalogAppBySlug(serviceSlug) {
const slug = sanitizeServiceSlug(serviceSlug);
try {
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC optional service fallback" });
return snapshot.data.services.find((candidate) => sanitizeServiceSlug(candidate.slug) === slug) || null;
} catch {
return null;
}
}
function buildOptionalServiceFallbackUrl(serviceSlug, app, fallbackPath) {
const path = sanitizeReturnTo(fallbackPath || "/");
const serviceApp = app || findServiceCatalogAppBySlug(serviceSlug);
let baseUrl = config.appBaseUrl;
if (isBimViewerServiceSlug(serviceSlug)) {
baseUrl = getBimViewerBaseUrl(serviceApp);
} else if (isEngineServiceSlug(serviceSlug)) {
baseUrl = getEngineLaunchTargetUrl(serviceApp);
} else if (serviceSlug === "task-manager") {
baseUrl = getTaskBaseUrl();
} else if (serviceApp?.openUrl || serviceApp?.url) {
baseUrl = serviceApp.openUrl || serviceApp.url;
}
try {
return new URL(path, String(baseUrl || config.appBaseUrl).replace(/\/$/, "")).toString();
} catch {
return new URL("/", config.appBaseUrl).toString();
}
}
function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, returnTo = "/", session = null) {
if (isEngineServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff("nodedc", runtimeContext.user, { launcherSessionId: session?.id });
const targetUrl = new URL("/auth/nodedc/handoff/", getEngineLaunchTargetUrl(app));
const nextPath = sanitizeReturnTo(returnTo);
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
return;
}
if (isBimViewerServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id });
const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app));
const nextPath = sanitizeReturnTo(returnTo);
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
return;
}
if (serviceSlug !== "task-manager") {
res.redirect(app.openUrl || app.url || "/");
return;
}
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id });
const targetUrl = new URL("/auth/nodedc/handoff/", getTaskBaseUrl());
const nextPath = sanitizeReturnTo(returnTo);
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
}
function getEnginePublicBaseUrl() {
return String(
process.env.NODEDC_ENGINE_PUBLIC_URL ||
process.env.NODEDC_ENGINE_BASE_URL ||
process.env.ENGINE_BASE_URL ||
"https://engine.nodedc.ru"
).replace(/\/$/, "");
}
function getEngineBaseUrl() {
return String(config.engineBaseUrl || "https://engine.nodedc.ru").replace(/\/$/, "");
}
function getEngineLaunchTargetUrl(service) {
return String(service?.launchTargetUrl || getEnginePublicBaseUrl()).replace(/\/$/, "");
}
function buildAssistantActionProxyCommand(body, context) {
const source = isPlainRecord(body) ? body : {};
const sourceInput = isPlainRecord(source.input) ? source.input : source;
const input = stripClientAssistantContext(sourceInput);
const ownerContext = resolveAssistantActionOwnerContext(context, input);
const phase = normalizeAssistantActionPhase(source.phase || source.mode);
const confirmationToken = normalizeOptionalText(source.confirmationToken || sourceInput.confirmationToken || source.confirmation?.token);
return {
phase,
input: {
...input,
...ownerContext,
},
...(confirmationToken ? { confirmationToken } : {}),
};
}
function stripClientAssistantContext(input) {
const result = {};
const blockedKeys = new Set([
"actorUserId",
"actorEmail",
"actorSubject",
"groups",
"launcherGlobalRole",
"launcherUserStatus",
"globalStatus",
"membershipRole",
"membershipStatus",
"assistantRole",
"coreAssistantRole",
"isRoot",
"isSuperAdmin",
"removesOwnAccess",
"hasAlternativeAdminPath",
]);
for (const [key, value] of Object.entries(isPlainRecord(input) ? input : {})) {
if (blockedKeys.has(key)) continue;
result[key] = value;
}
return result;
}
function resolveAssistantActionOwnerContext({ actor, data, groups }, input) {
const groupSet = new Set(groups || []);
const isRoot = groupSet.has("nodedc:superadmin") || groupSet.has("nodedc:launcher:admin");
const actorMembership = isRoot ? null : resolveAssistantActionActorMembership(data, actor, input);
const assistantRole = isRoot ? "admin" : actorMembership?.coreAssistantRole || "member";
const membershipRole = isRoot ? "client_owner" : actorMembership?.role || "member";
const membershipStatus = isRoot ? "active" : actorMembership?.status || "disabled";
const selfTarget = isAssistantActionSelfTarget(data, actor, input);
const removesOwnAccess = selfTarget && assistantActionRemovesOwnAccess(input);
return {
assistantRole,
launcherGlobalRole: isRoot ? "root_admin" : membershipRole,
launcherUserStatus: actor.globalStatus || "active",
membershipRole,
membershipStatus,
removesOwnAccess,
hasAlternativeAdminPath: removesOwnAccess ? hasAlternativeAssistantAdminPath(data, actor, groups, input) : false,
};
}
function resolveAssistantActionActorMembership(data, actor, input) {
const activeMemberships = data.memberships.filter((membership) => membership.userId === actor.id && membership.status === "active");
const targetClientId = resolveAssistantActionTargetClientId(data, input);
if (targetClientId) {
const exact = activeMemberships.find((membership) => membership.clientId === targetClientId);
if (exact) return exact;
}
return activeMemberships
.slice()
.sort((left, right) => assistantMembershipWeight(right) - assistantMembershipWeight(left))[0] || null;
}
function resolveAssistantActionTargetClientId(data, input) {
const explicitClientId = normalizeOptionalText(input.clientId);
if (explicitClientId) return explicitClientId;
const targetMembershipId = normalizeOptionalText(input.membershipId);
if (targetMembershipId) {
return data.memberships.find((membership) => membership.id === targetMembershipId)?.clientId || null;
}
const targetUserId = normalizeOptionalText(input.targetUserId);
if (targetUserId) {
return data.memberships.find((membership) => membership.userId === targetUserId && membership.status === "active")?.clientId || null;
}
return null;
}
function isAssistantActionSelfTarget(data, actor, input) {
const targetUserId = normalizeOptionalText(input.targetUserId);
if (targetUserId && targetUserId === actor.id) return true;
const targetMembershipId = normalizeOptionalText(input.membershipId);
if (!targetMembershipId) return false;
return data.memberships.some((membership) => membership.id === targetMembershipId && membership.userId === actor.id);
}
function assistantActionRemovesOwnAccess(input) {
const actionId = normalizeOptionalText(input.actionId);
if (actionId === "hub.user.block" || actionId === "hub.membership.disable") return true;
if (actionId === "hub.assistant_access.change_role") {
const targetAssistantRole = normalizeOptionalText(input.targetAssistantRole);
return targetAssistantRole === "blocked" || targetAssistantRole === "member";
}
if (actionId === "hub.membership.change_role") {
return normalizeOptionalText(input.targetRole) === "member";
}
return false;
}
function hasAlternativeAssistantAdminPath(data, actor, groups, input) {
const groupSet = new Set(groups || []);
if (groupSet.has("nodedc:superadmin") || groupSet.has("nodedc:launcher:admin")) {
return normalizeOptionalText(input.actionId) !== "hub.user.block";
}
const targetMembershipId = normalizeOptionalText(input.membershipId);
return data.memberships.some(
(membership) =>
membership.userId === actor.id &&
membership.id !== targetMembershipId &&
membership.status === "active" &&
membership.coreAssistantRole === "admin" &&
clientAdminMembershipRoles.has(membership.role)
);
}
function assistantMembershipWeight(membership) {
const assistantWeight = membership.coreAssistantRole === "admin" ? 100 : membership.coreAssistantRole === "blocked" ? -100 : 0;
const roleWeight = membership.role === "client_owner" ? 30 : membership.role === "client_admin" ? 20 : 10;
return assistantWeight + roleWeight;
}
function normalizeAssistantActionPhase(value) {
const phase = String(value || "preview").trim().toLowerCase().replace(/_/g, "-");
return ["preview", "dry-run", "execute"].includes(phase) ? phase : "preview";
}
function getAiWorkspaceAssistantBaseUrl() {
return String(
process.env.NDC_AI_WORKSPACE_ASSISTANT_INTERNAL_URL ||
process.env.AI_WORKSPACE_ASSISTANT_INTERNAL_URL ||
process.env.NDC_AI_WORKSPACE_ASSISTANT_URL ||
process.env.AI_WORKSPACE_ASSISTANT_URL ||
"http://localhost:18082"
).replace(/\/$/, "");
}
async function requestAiWorkspaceAssistantJson(pathname, init = {}) {
if (!config.internalAccessToken) {
throw new Error("NODE.DC internal access token is not configured");
}
const targetUrl = new URL(pathname, `${getAiWorkspaceAssistantBaseUrl()}/`);
const response = await fetch(targetUrl, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${config.internalAccessToken}`,
"X-NODEDC-User-Id": init.actor?.id || "",
"X-NODEDC-User-Email": init.actor?.email || "",
"X-NODEDC-User-Role": init.role || "",
"X-NODEDC-User-Groups": Array.isArray(init.groups) ? init.groups.join(",") : "",
...(init.headers ?? {}),
},
body: JSON.stringify(init.body ?? {}),
});
const text = await response.text();
const payload = text ? parseJsonResponse(text, targetUrl.toString()) : {};
if (!response.ok) {
const error = typeof payload?.error === "string" ? payload.error : `AI Workspace Assistant API failed: ${response.status}`;
throw new Error(error);
}
return payload;
}
async function requestTaskManagerInternalJson(pathname, init = {}) {
if (!config.internalAccessToken) {
throw new Error("NODE.DC internal access token is not configured");
}
const targetUrl = new URL(pathname, `${getTaskBaseUrl()}/`);
const hasBody = typeof init.body === "object" && init.body !== null;
const response = await fetch(targetUrl, {
method: init.method ?? (hasBody ? "POST" : "GET"),
headers: {
Accept: "application/json",
Authorization: `Bearer ${config.internalAccessToken}`,
...(hasBody ? { "Content-Type": "application/json" } : {}),
...(init.headers ?? {}),
},
body: hasBody ? JSON.stringify(init.body) : undefined,
});
const text = await response.text();
const payload = text ? parseJsonResponse(text, targetUrl.toString()) : {};
if (!response.ok) {
const error = typeof payload?.error === "string" ? payload.error : `Task Manager internal API failed: ${response.status}`;
throw new Error(error);
}
return payload;
}
async function requestEngineInternalJson(pathname, init = {}) {
if (!config.internalAccessToken) {
throw new Error("NODE.DC internal access token is not configured");
}
const targetUrl = new URL(pathname, `${getEngineBaseUrl()}/`);
const hasBody = typeof init.body === "object" && init.body !== null;
const response = await fetch(targetUrl, {
method: init.method ?? (hasBody ? "POST" : "GET"),
headers: {
Accept: "application/json",
Authorization: `Bearer ${config.internalAccessToken}`,
"X-Authentik-Groups": "nodedc_admin",
"X-Authentik-Email": "launcher-internal@nodedc.ru",
"X-Authentik-Username": "launcher-internal@nodedc.ru",
...(hasBody ? { "Content-Type": "application/json" } : {}),
...(init.headers ?? {}),
},
body: hasBody ? JSON.stringify(init.body) : undefined,
});
const text = await response.text();
const payload = text ? parseJsonResponse(text, targetUrl.toString()) : {};
if (!response.ok) {
const error = typeof payload?.error === "string" ? payload.error : `Engine internal API failed: ${response.status}`;
throw new Error(error);
}
return payload;
}
async function syncTaskManagerUserProfile(user) {
if (!user?.email || !config.internalAccessToken) {
return null;
}
const taskerNameParts = splitTaskerProfileName(user.name);
try {
return await requestTaskManagerInternalJson("/api/internal/nodedc/users/profile-sync/", {
method: "POST",
body: {
email: user.email,
subject: user.authentikUserId ?? undefined,
name: user.name,
displayName: user.name,
firstName: taskerNameParts.firstName,
lastName: taskerNameParts.lastName,
avatarUrl: resolveUserAvatarPublicUrl(user),
},
});
} catch (error) {
console.warn(
error instanceof Error
? `Task Manager profile sync failed: ${error.message}`
: "Task Manager profile sync failed"
);
return null;
}
}
async function cleanupTaskManagerUserAccess(user, options = {}) {
if (!user?.email || !config.internalAccessToken) {
return null;
}
try {
return await requestTaskManagerInternalJson("/api/internal/nodedc/logout/", {
method: "POST",
body: {
source: normalizeOptionalText(options.source) ?? "launcher-user-access-revoked",
subject: user.authentikUserId ?? undefined,
email: user.email,
revokeIdentityLinks: options.revokeIdentityLinks === true,
revokeTaskerAccess: options.revokeTaskerAccess !== false,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Task Manager user cleanup failed";
console.warn(`Task Manager user cleanup failed: ${message}`);
return { ok: false, error: message };
}
}
function parseJsonResponse(text, url) {
try {
return JSON.parse(text);
} catch {
throw new Error(`Task Manager internal API returned non-JSON response: ${url}`);
}
}
function normalizeOptionalText(value) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function isPlainRecord(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function normalizeTaskManagerRole(value) {
return value === "guest" || value === "admin" || value === "member" ? value : null;
}
function isPublicTaskManagerGuestServiceAssignment(data, payload) {
if (payload?.value !== "viewer") return false;
const service = data.services.find((candidate) => candidate.id === payload?.serviceId);
if (!service || !isTaskManagerService(service)) return false;
return data.memberships.some(
(membership) =>
membership.userId === payload?.userId &&
membership.clientId === publicPoolClientId &&
membership.status === "active"
);
}
function isTaskManagerService(service) {
return service?.slug === "task-manager" || service?.authentikApplicationSlug === "task-manager";
}
function resolveTaskManagerRoleForMembership(role) {
return role === "client_owner" || role === "client_admin" ? "admin" : "member";
}
function normalizeTaskManagerWorkspaceManagedBy(value) {
return value === "tasker" ? "tasker" : "launcher";
}
function getClientTaskManagerWorkspaces(client) {
const taskManager = client?.integrations?.taskManager;
const workspaces = Array.isArray(taskManager?.workspaces) ? taskManager.workspaces : [];
const legacySlug = normalizeOptionalText(taskManager?.workspaceSlug);
if (!legacySlug || workspaces.some((workspace) => normalizeOptionalText(workspace?.slug) === legacySlug)) {
return workspaces;
}
return [
...workspaces,
{
slug: legacySlug,
name: normalizeOptionalText(taskManager?.workspaceName),
isPrimary: true,
managedBy: "launcher",
},
];
}
function resolveTaskManagerWorkspaceBinding(client, workspaceSlug) {
const normalizedWorkspaceSlug = normalizeOptionalText(workspaceSlug);
if (!normalizedWorkspaceSlug) return null;
return (
getClientTaskManagerWorkspaces(client).find((workspace) => normalizeOptionalText(workspace?.slug) === normalizedWorkspaceSlug) ?? null
);
}
function resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug) {
return normalizeTaskManagerWorkspaceManagedBy(resolveTaskManagerWorkspaceBinding(client, workspaceSlug)?.managedBy);
}
function resolveTaskManagerWorkspaceAssignments(data, user) {
if (!user?.id) return [];
const bySlug = new Map();
for (const membership of data.taskManagerMemberships ?? []) {
if (membership.userId !== user.id) continue;
const workspaceSlug = normalizeOptionalText(membership.workspaceSlug);
if (!workspaceSlug) continue;
const client = data.clients.find((candidate) => candidate.id === membership.clientId);
const managedBy = normalizeTaskManagerWorkspaceManagedBy(
membership.managedBy ?? resolveTaskManagerWorkspaceBinding(client, workspaceSlug)?.managedBy
);
const current = bySlug.get(workspaceSlug);
if (current && current.managedBy === "launcher") continue;
bySlug.set(workspaceSlug, {
slug: workspaceSlug,
name: normalizeOptionalText(membership.workspaceName ?? resolveTaskManagerWorkspaceBinding(client, workspaceSlug)?.name),
managedBy,
clientId: client?.id ?? membership.clientId ?? null,
clientName: client?.name ?? null,
role: normalizeTaskManagerRole(membership.role) ?? "member",
});
}
return [...bySlug.values()];
}
function createServiceHandoff(serviceSlug, user, { launcherSessionId = null } = {}) {
pruneExpiredServiceHandoffs();
const token = randomBase64Url(48);
serviceHandoffs.set(token, {
serviceSlug,
launcherSessionId: typeof launcherSessionId === "string" && launcherSessionId ? launcherSessionId : null,
user: {
sub: user.sub,
email: user.email,
name: user.name,
preferredUsername: user.preferredUsername ?? user.email,
avatarUrl: user.avatarUrl ?? null,
groups: user.groups ?? [],
},
expiresAt: Date.now() + serviceHandoffTtlMs,
});
return token;
}
function consumeServiceHandoff(token, serviceSlug) {
pruneExpiredServiceHandoffs();
if (!token) {
return null;
}
const handoff = serviceHandoffs.get(token);
serviceHandoffs.delete(token);
if (!handoff || handoff.expiresAt < Date.now() || handoff.serviceSlug !== serviceSlug) {
return null;
}
return handoff;
}
function pruneExpiredServiceHandoffs() {
const now = Date.now();
for (const [token, handoff] of serviceHandoffs.entries()) {
if (!handoff || handoff.expiresAt < now) {
serviceHandoffs.delete(token);
}
}
}
function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess, user, workspaceSlug = null, serviceModules = {}) {
const mode = data.settings?.taskManager?.workspaceCreationPolicy ?? "any_authorized_user";
const groupSet = new Set(groups);
const isSuperAdmin = groupSet.has("nodedc:superadmin");
const isTaskManagerAdmin = groupSet.has("nodedc:taskmanager:admin");
const workspaces = resolveTaskManagerWorkspaceAssignments(data, user);
const hasLauncherManagedWorkspace = workspaces.some((workspace) => workspace.managedBy === "launcher");
const isPublicPoolUser = data.memberships.some(
(membership) => membership.userId === user?.id && membership.clientId === publicPoolClientId && membership.status === "active"
);
const defaultManagedBy = hasLauncherManagedWorkspace && !isSuperAdmin ? "launcher" : "tasker";
const defaultInviteApproval = hasLauncherManagedWorkspace && !isSuperAdmin ? "launcher" : isPublicPoolUser ? "nodedc" : "tasker";
const workspaceAssignment =
typeof workspaceSlug === "string" && workspaceSlug.trim()
? workspaces.find((workspace) => workspace.slug === workspaceSlug.trim())
: null;
if (!hasTaskManagerAccess) {
return {
mode,
managedBy: defaultManagedBy,
defaultManagedBy,
inviteApproval: "disabled",
defaultInviteApproval,
workspaces,
serviceModules,
canCreateWorkspace: false,
reason: "Нет доступа к Operational Core.",
};
}
if (mode === "disabled") {
return {
mode,
managedBy: defaultManagedBy,
defaultManagedBy,
inviteApproval: "disabled",
defaultInviteApproval,
workspaces,
serviceModules,
canCreateWorkspace: false,
reason: "Создание рабочих пространств отключено на уровне платформы.",
};
}
if (hasLauncherManagedWorkspace && !isSuperAdmin && !isTaskManagerAdmin) {
if (workspaceAssignment?.managedBy === "launcher") {
return {
mode,
managedBy: "launcher",
defaultManagedBy: "launcher",
inviteApproval: "launcher",
defaultInviteApproval: "launcher",
workspaces,
serviceModules,
canCreateWorkspace: false,
reason: "Рабочие пространства этого пользователя управляются через Launcher.",
};
}
if (workspaceSlug) {
return {
mode,
managedBy: "tasker",
defaultManagedBy: "launcher",
inviteApproval: defaultInviteApproval,
defaultInviteApproval,
workspaces,
serviceModules,
canCreateWorkspace: false,
reason: "Self-service workspace работает через Tasker, approve инвайтов выполняет Launcher.",
};
}
return {
mode,
managedBy: "launcher",
defaultManagedBy: "launcher",
inviteApproval: "launcher",
defaultInviteApproval: "launcher",
workspaces,
serviceModules,
canCreateWorkspace: false,
reason: "Рабочие пространства этого пользователя управляются через Launcher.",
};
}
if (mode === "task_admins_only" && !isSuperAdmin && !isTaskManagerAdmin) {
return {
mode,
managedBy: defaultManagedBy,
defaultManagedBy,
inviteApproval: defaultInviteApproval,
defaultInviteApproval,
workspaces,
serviceModules,
canCreateWorkspace: false,
reason: "Создание рабочих пространств доступно только администраторам Operational Core.",
};
}
return {
mode,
managedBy: "tasker",
defaultManagedBy: "tasker",
inviteApproval: defaultInviteApproval,
defaultInviteApproval,
workspaces,
serviceModules,
canCreateWorkspace: true,
reason: "Создание рабочих пространств разрешено платформенной policy.",
};
}
function resolveTaskManagerWorkspaceServiceModules(data, user, serviceSlug, workspaceSlug) {
const normalizedWorkspaceSlug = normalizeOptionalText(workspaceSlug);
if (!normalizedWorkspaceSlug) {
return {};
}
const workspaces = resolveTaskManagerWorkspaceAssignments(data, user);
const workspaceAssignment = workspaces.find((workspace) => workspace.slug === normalizedWorkspaceSlug);
const boundClientId = resolveTaskManagerWorkspaceClientId(data, normalizedWorkspaceSlug);
const clientId =
workspaceAssignment?.clientId ??
(boundClientId ? null : isPublicPoolUser(data, user) ? publicPoolClientId : null);
return resolveUserServiceModules(data, user, serviceSlug, clientId);
}
function resolveTaskManagerWorkspaceClientId(data, workspaceSlug) {
const normalizedWorkspaceSlug = normalizeOptionalText(workspaceSlug);
if (!normalizedWorkspaceSlug) return null;
const client = data.clients.find((candidate) => resolveTaskManagerWorkspaceBinding(candidate, normalizedWorkspaceSlug));
return client?.id ?? null;
}
function isPublicPoolUser(data, user) {
return data.memberships.some(
(membership) => membership.userId === user?.id && membership.clientId === publicPoolClientId && membership.status === "active"
);
}
function resolveUserServiceModules(data, user, serviceSlug, clientId) {
if (!user?.id) return {};
const service = data.services.find(
(candidate) => candidate.slug === serviceSlug || candidate.authentikApplicationSlug === serviceSlug
);
if (!service?.id) return {};
if (hasSystemServiceModuleEntitlement(user, service, "codex_agents")) {
return { codex_agents: true };
}
return Object.fromEntries(
(data.serviceModuleEntitlements ?? [])
.filter(
(entitlement) =>
entitlement.userId === user.id &&
entitlement.serviceId === service.id &&
entitlement.enabled
)
.map((entitlement) => [entitlement.moduleId, true])
);
}
function hasSystemServiceModuleEntitlement(user, service, moduleId) {
return user?.id === "user_root" && moduleId === "codex_agents" && service?.slug === "task-manager";
}
function getFrontchannelLogoutUrls() {
const urls = [config.taskLogoutUrl];
const launcherData = readLauncherData();
const services = Array.isArray(launcherData?.services) ? launcherData.services : [];
for (const service of services) {
if (typeof service.logoutUrl === "string" && service.logoutUrl.trim()) {
urls.push(service.logoutUrl.trim());
}
}
return [...new Set(urls.map(normalizeLogoutUrl).filter(Boolean))];
}
async function notifyTaskSessionLogout(session) {
if (!session?.user || !config.internalAccessToken || !config.taskInternalLogoutUrl) {
return;
}
const runtimeContext = getRuntimeSessionContext(session);
const user = runtimeContext.user ?? session.user;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1500);
try {
const response = await fetch(config.taskInternalLogoutUrl, {
method: "POST",
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${config.internalAccessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "launcher-global-logout",
subject: session.user.sub,
email: user.email || session.user.email || null,
}),
signal: controller.signal,
});
if (!response.ok) {
console.warn(`Task internal logout returned ${response.status}`);
}
} catch (error) {
if (error?.name !== "AbortError") {
console.warn(error instanceof Error ? error.message : "Task internal logout failed");
}
} finally {
clearTimeout(timeout);
}
}
function normalizeLogoutUrl(value) {
try {
const url = new URL(value);
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
return url.toString();
} catch {
return null;
}
}
function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
const logoutUrlsJson = JSON.stringify(frontchannelLogoutUrls);
const redirectUrlJson = JSON.stringify(finalRedirectUrl);
return `<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NODE.DC</title>
<style>
html,body{height:100%;margin:0;background:#0b0f0e;color:#0b0f0e}
</style>
</head>
<body aria-busy="true">
<script>
const eventPayload = {
type: "nodedc:session:logout",
id: globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : String(Date.now()) + "-" + Math.random().toString(36).slice(2),
source: "launcher-global-logout",
createdAt: Date.now()
};
try {
const channel = new BroadcastChannel("nodedc-platform-session");
channel.postMessage(eventPayload);
channel.close();
} catch {}
try {
localStorage.setItem("nodedc:platform-session-event", JSON.stringify(eventPayload));
} catch {}
const logoutUrls = ${logoutUrlsJson};
const finalRedirectUrl = ${redirectUrlJson};
for (const logoutUrl of logoutUrls) {
fetch(logoutUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined);
const image = new Image();
image.referrerPolicy = "no-referrer";
image.src = logoutUrl;
}
window.setTimeout(() => window.location.replace(finalRedirectUrl), 50);
</script>
</body>
</html>`;
}
function renderSessionSyncBridgePage(allowedOrigins) {
const allowedOriginsJson = JSON.stringify(allowedOrigins);
return `<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NODE.DC session sync</title>
</head>
<body>
<script>
const allowedOrigins = new Set(${allowedOriginsJson});
const parentOrigin = (() => {
try {
return new URL(document.referrer).origin;
} catch {
return "";
}
})();
const channelName = "nodedc-platform-session";
const storageKey = "nodedc:platform-session-event";
let lastEventId = null;
function isAllowedOrigin(origin) {
return allowedOrigins.has(origin);
}
function isLogoutEvent(payload) {
return payload && payload.type === "nodedc:session:logout" && typeof payload.id === "string";
}
function forwardToParent(payload) {
if (!isLogoutEvent(payload) || payload.id === lastEventId || !isAllowedOrigin(parentOrigin)) return;
lastEventId = payload.id;
window.parent.postMessage(payload, parentOrigin);
}
function publish(payload) {
if (!isLogoutEvent(payload)) return;
try {
const channel = new BroadcastChannel(channelName);
channel.postMessage(payload);
channel.close();
} catch {}
try {
localStorage.setItem(storageKey, JSON.stringify(payload));
} catch {}
}
window.addEventListener("message", (event) => {
if (!isAllowedOrigin(event.origin) || !isLogoutEvent(event.data)) return;
publish(event.data);
});
try {
const channel = new BroadcastChannel(channelName);
channel.addEventListener("message", (event) => forwardToParent(event.data));
} catch {}
window.addEventListener("storage", (event) => {
if (event.key !== storageKey || !event.newValue) return;
try {
forwardToParent(JSON.parse(event.newValue));
} catch {}
});
</script>
</body>
</html>`;
}
function getSessionSyncAllowedOrigins() {
const origins = new Set([new URL(config.appBaseUrl).origin]);
for (const logoutUrl of getFrontchannelLogoutUrls()) {
try {
origins.add(new URL(logoutUrl).origin);
} catch {
void 0;
}
}
return [...origins];
}
function readLauncherData() {
try {
return controlPlaneStore.readData();
} catch {
return null;
}
}
function buildPublicBrandResponse(settings) {
const configuredLogoLinkUrl = settings?.brand?.logoLinkUrl || "/";
return {
logoLinkUrl: resolvePublicUrl(configuredLogoLinkUrl, config.appBaseUrl),
};
}
function resolvePublicUrl(value, baseUrl) {
try {
return new URL(value || "/", baseUrl).toString();
} catch {
return new URL("/", baseUrl).toString();
}
}
function resolveUserAvatarPublicUrl(user) {
if (!user?.avatarUrl) return null;
return resolvePublicUrl(user.avatarUrl, config.appBaseUrl);
}
async function saveUploadedFile(payload) {
if (!isUploadPayload(payload)) {
throw new Error("Некорректный payload загрузки");
}
const match = /^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/s.exec(payload.dataUrl);
if (!match) {
throw new Error("Файл должен прийти data-url с base64");
}
const mimeType = payload.mimeType || match[1] || "application/octet-stream";
const storedName = buildStoredFileName(payload.fileName, mimeType);
const fileBuffer = Buffer.from(match[2], "base64");
await Promise.all(
getWritableUploadRoots().map(async (uploadRoot) => {
await mkdir(uploadRoot, { recursive: true });
await writeFile(join(uploadRoot, storedName), fileBuffer);
})
);
return {
ok: true,
url: `/storage/uploads/${storedName}`,
fileName: storedName,
originalFileName: payload.fileName,
mimeType,
};
}
async function saveLauncherData(payload) {
await controlPlaneStore.writeData(payload);
}
function getWritableUploadRoots() {
return [runtimeUploadsRoot];
}
function getReadableUploadRoots() {
const roots = [
runtimeUploadsRoot,
join(projectRoot, "public", "storage", "uploads"),
];
const distRoot = join(projectRoot, "dist");
if (existsSync(distRoot)) {
roots.push(join(distRoot, "storage", "uploads"));
}
return [...new Set(roots)];
}
function buildStoredFileName(fileName, mimeType) {
const extension = extname(fileName) || extensionFromMimeType(mimeType);
const rawBase = fileName.slice(0, extension ? -extension.length : undefined);
const safeBase =
rawBase
.normalize("NFKD")
.replace(/[^\w.-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || "upload";
return `${Date.now()}-${randomUUID().slice(0, 8)}-${safeBase}${extension.toLowerCase()}`;
}
function extensionFromMimeType(mimeType) {
if (mimeType === "image/jpeg") return ".jpg";
if (mimeType === "image/png") return ".png";
if (mimeType === "image/gif") return ".gif";
if (mimeType === "image/webp") return ".webp";
if (mimeType === "video/mp4") return ".mp4";
if (mimeType === "video/webm") return ".webm";
if (mimeType === "video/quicktime") return ".mov";
return "";
}
function resolveRuntimeStorageRoot(root) {
const configuredRoot = process.env.NODEDC_LAUNCHER_STORAGE_DIR;
if (configuredRoot && configuredRoot.trim()) {
return isAbsolute(configuredRoot) ? configuredRoot : resolve(root, configuredRoot);
}
return join(root, "server", "storage");
}
function resolveRuntimeUploadsRoot(root, storageRoot) {
const configuredRoot = process.env.NODEDC_LAUNCHER_UPLOADS_DIR;
if (configuredRoot && configuredRoot.trim()) {
return isAbsolute(configuredRoot) ? configuredRoot : resolve(root, configuredRoot);
}
return join(storageRoot, "uploads");
}
function isUploadPayload(payload) {
return Boolean(
payload &&
typeof payload === "object" &&
typeof payload.fileName === "string" &&
typeof payload.mimeType === "string" &&
typeof payload.dataUrl === "string"
);
}
function getCurrentSession(req) {
const sessionId = parseCookies(req.headers.cookie)[sessionCookieName];
if (!sessionId) return null;
const session = sessions.get(sessionId);
if (!session || session.expiresAt < Date.now()) {
sessions.delete(sessionId);
return null;
}
return session;
}
function expireSession(res, session) {
sessions.delete(session.id);
res.clearCookie(sessionCookieName, clearCookieOptions());
}
function rejectInactiveSession(res, session, sessionAccess) {
expireSession(res, session);
res.status(sessionAccess.status ?? 401).json({
authenticated: false,
loginUrl: "/auth/login",
error: sessionAccess.error,
message: sessionAccess.message,
});
}
function pruneExpiredSessions() {
for (const [sessionId, session] of sessions) {
if (session.expiresAt < Date.now()) {
sessions.delete(sessionId);
}
}
}
function pruneExpiredState() {
for (const [state, pendingLogin] of pendingLogins) {
if (pendingLogin.expiresAt < Date.now()) {
pendingLogins.delete(state);
}
}
}
function getValidOidcCookieStates(req) {
const rawValue = parseCookies(req.headers.cookie)[oidcStateCookieName];
if (!rawValue) return [];
const seen = new Set();
return rawValue
.split(".")
.filter((state) => /^[A-Za-z0-9_-]{32,256}$/.test(state))
.filter((state) => {
if (seen.has(state)) return false;
seen.add(state);
return true;
})
.filter((state) => {
const pendingLogin = pendingLogins.get(state);
return Boolean(pendingLogin && pendingLogin.expiresAt >= Date.now());
});
}
function setOidcStateCookie(res, states) {
if (!states.length) {
res.clearCookie(oidcStateCookieName, clearCookieOptions());
return;
}
res.cookie(oidcStateCookieName, states.join("."), cookieOptions(pendingLoginTtlMs));
}
function parseCookies(cookieHeader) {
if (!cookieHeader) return {};
return Object.fromEntries(
cookieHeader.split(";").flatMap((part) => {
const separatorIndex = part.indexOf("=");
if (separatorIndex === -1) return [];
const key = part.slice(0, separatorIndex).trim();
const value = part.slice(separatorIndex + 1).trim();
return [[key, decodeURIComponent(value)]];
})
);
}
function requireLauncherAdmin(req, res, next) {
const session = getCurrentSession(req);
if (!session) {
const assistantGatewayContext = resolveAssistantGatewayAdminContext(req);
if (assistantGatewayContext?.ok) {
req.nodedcSession = assistantGatewayContext.session;
req.nodedcAdminScope = assistantGatewayContext.adminScope;
req.nodedcAssistantGateway = assistantGatewayContext.gateway;
next();
return;
}
if (assistantGatewayContext) {
res.status(assistantGatewayContext.status).json({
ok: false,
error: assistantGatewayContext.error,
message: assistantGatewayContext.message,
});
return;
}
res.status(401).json({ authenticated: false, loginUrl: "/auth/login" });
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
rejectInactiveSession(res, session, sessionAccess);
return;
}
const runtimeContext = sessionAccess.runtimeContext;
const adminScope = resolveAdminScope(runtimeContext.user, runtimeContext.groups);
if (!adminScope.isRoot && adminScope.clientIds.size === 0) {
res.status(403).json({ error: "Недостаточно прав Launcher admin" });
return;
}
req.nodedcSession = { ...session, user: runtimeContext.user };
req.nodedcAdminScope = adminScope;
next();
}
function requireRootLauncherAdmin(req, res, next) {
if (!req.nodedcAdminScope?.isRoot) {
res.status(403).json({ error: "Действие доступно только суперпользователю NODE.DC" });
return;
}
next();
}
function requireSession(req, res, next) {
const session = getCurrentSession(req);
if (!session) {
res.status(401).json({ authenticated: false, loginUrl: "/auth/login" });
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
rejectInactiveSession(res, session, sessionAccess);
return;
}
const runtimeContext = sessionAccess.runtimeContext;
req.nodedcSession = { ...session, user: runtimeContext.user };
next();
}
function resolveAssistantGatewayAdminContext(req) {
const gateway = readRequestHeader(req, "x-nodedc-assistant-gateway");
const actorUserId = readRequestHeader(req, "x-nodedc-assistant-actor-user-id");
const actorEmail = readRequestHeader(req, "x-nodedc-assistant-actor-email");
const actorSubject = readRequestHeader(req, "x-nodedc-assistant-actor-subject");
const hasGatewaySignal = Boolean(gateway || actorUserId || actorEmail || actorSubject);
if (!hasGatewaySignal) {
return null;
}
if (gateway !== assistantGatewayName) {
return {
ok: false,
status: 401,
error: "assistant_gateway_invalid",
message: "Unknown assistant gateway.",
};
}
if (!isInternalRequestAuthorized(req)) {
return {
ok: false,
status: config.internalAccessToken ? 401 : 503,
error: config.internalAccessToken ? "assistant_gateway_unauthorized" : "assistant_gateway_not_configured",
message: "Assistant gateway internal token is missing or invalid.",
};
}
if (!isAssistantGatewayAdminRoute(req)) {
return {
ok: false,
status: 403,
error: "assistant_gateway_route_not_allowlisted",
message: "Assistant gateway can only call explicitly allowlisted admin routes.",
};
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NDC Core Assistant Gateway", source: "assistant-gateway" });
const actor = findInternalAccessUser(snapshot.data, {
userId: actorUserId,
email: actorEmail,
subject: actorSubject,
});
if (!actor) {
return {
ok: false,
status: 404,
error: "assistant_gateway_actor_not_found",
message: "Assistant gateway actor was not found in Launcher control-plane.",
};
}
if (actor.globalStatus === "blocked") {
return {
ok: false,
status: 403,
error: "assistant_gateway_actor_blocked",
message: "Assistant gateway actor is blocked.",
};
}
const groups = resolveRequiredGroups(snapshot.data, actor);
const user = buildAssistantGatewaySessionUser(actor, groups);
const adminScope = resolveAdminScope(user, groups);
if (!adminScope.isRoot && adminScope.clientIds.size === 0) {
return {
ok: false,
status: 403,
error: "assistant_gateway_actor_not_admin",
message: "Assistant gateway actor has no Launcher admin scope.",
};
}
return {
ok: true,
session: {
id: `assistant-gateway:${actor.id}`,
expiresAt: Date.now() + 60_000,
user,
},
adminScope,
gateway: {
name: gateway,
actorUserId: actor.id,
},
};
}
function isAssistantGatewayAdminRoute(req) {
const method = String(req.method || "").toUpperCase();
const path = req.path || String(req.url || "").split("?")[0] || "";
return assistantGatewayAdminRouteAllowlist.some((route) => route.method === method && route.pattern.test(path));
}
function buildAssistantGatewaySessionUser(actor, groups) {
return {
sub: actor.authentikUserId || `launcher:${actor.id}`,
email: actor.email,
name: actor.name,
avatarUrl: actor.avatarUrl ?? null,
groups,
};
}
function readRequestHeader(req, name) {
const raw = req.headers[String(name).toLowerCase()];
const value = Array.isArray(raw) ? raw[0] : raw;
return typeof value === "string" ? value.trim() : "";
}
function isInternalRequestAuthorized(req) {
if (!config.internalAccessToken) {
return false;
}
const authorization = typeof req.headers.authorization === "string" ? req.headers.authorization : "";
const bearerToken = authorization.match(/^Bearer\s+(.+)$/i)?.[1] ?? "";
const headerToken = typeof req.headers["x-nodedc-internal-token"] === "string" ? req.headers["x-nodedc-internal-token"] : "";
const requestToken = bearerToken || headerToken;
return safeTokenEquals(requestToken, config.internalAccessToken);
}
function safeTokenEquals(actual, expected) {
if (!actual || !expected) {
return false;
}
const actualBuffer = Buffer.from(String(actual));
const expectedBuffer = Buffer.from(String(expected));
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
}
function findInternalAccessUser(data, payload) {
const subject = typeof payload?.subject === "string" ? payload.subject : "";
const email = typeof payload?.email === "string" ? payload.email.toLowerCase() : "";
const canonicalEmail = canonicalInternalAccessEmail(email);
const userId = typeof payload?.userId === "string" ? payload.userId : "";
return (
data.users.find((user) => userId && user.id === userId) ??
data.users.find((user) => subject && user.authentikUserId === subject) ??
data.users.find((user) => email && user.email.toLowerCase() === email) ??
data.users.find((user) => canonicalEmail && canonicalInternalAccessEmail(user.email) === canonicalEmail) ??
null
);
}
function canonicalInternalAccessEmail(value) {
const email = String(value || "").trim().toLowerCase();
const [local, domain] = email.split("@");
if (!local || !domain) return email;
if (domain === "gmail.com" || domain === "googlemail.com") {
return `${local.replace(/\./g, "")}@gmail.com`;
}
return email;
}
function sanitizeServiceSlug(value) {
return typeof value === "string" && value ? value : "task-manager";
}
function getLauncherProfileContext(session) {
const snapshot = controlPlaneStore.getSnapshot(session.user);
if (snapshot.actor.source !== "launcher") {
throw new Error("Профиль пользователя не найден в Launcher control-plane");
}
return {
actor: snapshot.actor,
data: snapshot.data,
};
}
function findLauncherUser(data, userId) {
const user = data.users.find((candidate) => candidate.id === userId);
if (!user) {
throw new Error(`Unknown Launcher user: ${userId}`);
}
return user;
}
function notificationSubjectFromUser(user) {
return {
userId: user?.id ?? null,
email: typeof user?.email === "string" ? user.email.toLowerCase() : null,
};
}
function notificationDeliveryForUser(user, surface, notification) {
if (!user?.id && !user?.email) return null;
return {
recipientUserId: user?.id ?? null,
recipientEmail: typeof user?.email === "string" ? user.email.toLowerCase() : null,
surface,
title: notification.title,
body: notification.body ?? "",
meta: notification.meta ?? {},
actionable: notification.actionable === true,
actionType: notification.actionType ?? null,
actionUrl: notification.actionUrl ?? null,
entityType: notification.entityType ?? null,
entityId: notification.entityId ?? null,
};
}
function rootAdminNotificationDelivery(data, notification) {
const rootUser = data.users.find((user) => user.id === "user_root") ?? data.users[0] ?? null;
return notificationDeliveryForUser(rootUser, "hub", notification);
}
function notificationUsersForTaskerInvite(data, request) {
const users = [];
const seen = new Set();
const addUser = (user) => {
if (!user?.id || seen.has(user.id)) return;
seen.add(user.id);
users.push(user);
};
addUser(data.users.find((user) => user.id === request?.inviterUserId));
addUser(data.users.find((user) => normalizeNotificationEmail(user.email) === normalizeNotificationEmail(request?.inviterEmail)));
addUser(data.users.find((user) => normalizeNotificationEmail(user.email) === normalizeNotificationEmail(request?.inviteeEmail)));
return users;
}
function buildAccessRequestHubNotification(accessRequest) {
const applicantName = [accessRequest?.lastName, accessRequest?.firstName].filter(Boolean).join(" ") || accessRequest?.email || "NODE.DC user";
const status = accessRequest?.status || "new";
return {
title: status === "new" ? "Входящий запрос доступа" : "Запрос доступа обновлён",
body: `${applicantName} · ${accessRequest?.email || ""}`,
meta: {
displayStatus: platformNotificationStatusLabel(status),
meta: accessRequest?.company || formatNotificationDate(accessRequest?.createdAt),
},
};
}
function buildTaskerInviteHubNotification(request) {
const status = request?.status || "new";
return {
title: status === "new" ? "Запрос доступа Operational Core" : "Operational Core: заявка обновлена",
body: `${request?.workspaceName || request?.workspaceSlug || "Operational Core"} · ${request?.inviteeEmail || ""}`,
meta: {
displayStatus: platformNotificationStatusLabel(status),
meta: request?.inviterName || formatNotificationDate(request?.createdAt),
},
};
}
function buildEngineRoleHubNotification(request) {
const target = request?.targetName || request?.targetEmail || "Engine user";
const status = request?.status || "new";
return {
title: engineRoleHubNotificationTitle(request),
body: engineRoleHubNotificationBody(request, target),
meta: {
displayStatus: platformNotificationStatusLabel(status),
meta: `Запрошено: ${engineServiceRoleLabel(request?.requestedServiceRole)}`,
requestId: request?.id,
requestedServiceRole: request?.requestedServiceRole ?? null,
currentServiceRole: request?.currentServiceRole ?? null,
},
};
}
function buildEngineWorkflowHubNotification(request) {
const status = request?.status || "new";
return {
title: status === "new" ? "Запрос доступа к Engine workflow" : "Engine: заявка обновлена",
body: `${request?.workflowName || request?.workflowId || "Engine workflow"} · ${request?.targetEmail || ""}`,
meta: {
displayStatus: platformNotificationStatusLabel(status),
meta: request?.requesterName || formatNotificationDate(request?.createdAt),
requestId: request?.id,
},
};
}
function buildEngineSurfaceNotification(request) {
if (request?.requestType === "service_role") {
return {
title: request?.workflowName || "NODE.DC Engine",
body: engineRoleSurfaceNotificationBody(request),
meta: {
displayStatus: engineSurfaceStatusLabel(request),
meta: engineSurfaceMeta(request),
requestId: request?.id,
requestedServiceRole: request?.requestedServiceRole ?? null,
currentServiceRole: request?.currentServiceRole ?? null,
},
};
}
return {
title: request?.workflowName || request?.workflowId || "Engine workflow",
body: `${request?.requesterName || request?.requesterEmail || "NODE.DC Engine"} просит роль ${engineWorkflowRoleLabel(request?.role)}`,
meta: {
displayStatus: engineSurfaceStatusLabel(request),
meta: engineSurfaceMeta(request),
requestId: request?.id,
},
};
}
function engineRoleHubNotificationTitle(request) {
if (request?.status === "approved") return request?.comment === "admin_role_changed" ? "Engine: роль изменена" : "Engine: роль обновлена";
if (request?.status === "rejected") return "Engine: повышение роли отклонено";
return "Engine: запрос повышения роли";
}
function engineRoleHubNotificationBody(request, target) {
if (request?.status === "rejected") return `${target} · администратор отклонил заявку`;
if (request?.status === "approved") {
return request?.comment === "admin_role_changed" ? `${target} · роль изменена администратором` : `${target} · новая роль применена`;
}
return `${target} · текущая роль ${engineServiceRoleLabel(request?.currentServiceRole)}`;
}
function engineRoleSurfaceNotificationBody(request) {
const requestedRole = engineServiceRoleLabel(request?.requestedServiceRole);
const requestedByTarget = normalizeNotificationEmail(request?.requesterEmail) === normalizeNotificationEmail(request?.targetEmail);
if (request?.status === "approved") {
return requestedByTarget && request?.comment !== "admin_role_changed"
? `Администратор подтвердил повышение роли: ${requestedRole}`
: `Администратор изменил роль: ${requestedRole}`;
}
if (request?.status === "rejected") {
return "Администратор отклонил заявку на повышение роли";
}
return `${request?.requesterName || request?.requesterEmail || "NODE.DC Engine"} просит роль ${requestedRole}`;
}
function engineSurfaceStatusLabel(request) {
if (request?.requestType === "service_role" && request?.status === "approved" && request?.comment === "admin_role_changed") {
return "Изменено";
}
if (request?.status === "approved") return "Одобрено";
if (request?.status === "rejected") return "Отклонено";
return "Новая заявка";
}
function engineSurfaceMeta(request) {
const createdAt = formatShortNotificationDate(request?.createdAt);
return ["Ваш запрос", createdAt].filter(Boolean).join(" · ");
}
function engineServiceRoleLabel(role) {
if (role === "member") return "Участник";
if (role === "viewer") return "Гость";
return "не назначена";
}
function engineWorkflowRoleLabel(role) {
if (role === "admin") return "Соавтор";
if (role === "editor") return "Редактор";
return "Просмотр";
}
function platformNotificationStatusLabel(status) {
const normalized = String(status || "").trim().toLowerCase();
if (normalized === "approved") return "Подтверждено";
if (normalized === "rejected") return "Отклонено";
if (normalized === "cancelled") return "Отменено";
return "Входящее";
}
function formatNotificationDate(value) {
const timestamp = Date.parse(String(value || ""));
if (!Number.isFinite(timestamp)) return "";
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(timestamp));
}
function formatShortNotificationDate(value) {
const timestamp = Date.parse(String(value || ""));
if (!Number.isFinite(timestamp)) return "";
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(timestamp));
}
function normalizeNotificationEmail(value) {
return String(value || "").trim().toLowerCase();
}
function sanitizeNotificationSurface(value, fallback = "hub") {
const surface = typeof value === "string" && value.trim() ? value.trim().toLowerCase() : fallback;
if (surface === "hub" || surface === "engine" || surface === "operational-core") return surface;
return fallback;
}
function publishNotificationEvent(command) {
if (!notificationCoreClient.isConfigured()) {
return;
}
if (!Array.isArray(command?.deliveries) || command.deliveries.length === 0) {
return;
}
notificationCoreClient.createEvent(command).catch((error) => {
console.warn("[notification-core] event publish failed:", error?.message || error);
});
}
function isLauncherAdmin(groups) {
return groups.includes("nodedc:superadmin") || groups.includes("nodedc:launcher:admin");
}
const clientAdminMembershipRoles = new Set(["client_owner", "client_admin"]);
const protectedLauncherUserIds = new Set(["user_root"]);
function resolveAdminScope(identity, groups) {
const snapshot = controlPlaneStore.getSnapshot(identity);
const actorId = snapshot.actor.source === "launcher" ? snapshot.actor.id : null;
const isRoot = isLauncherAdmin(groups);
const clientIds = new Set(
isRoot
? snapshot.data.clients.map((client) => client.id)
: snapshot.data.memberships
.filter((membership) => membership.userId === actorId && membership.status === "active" && clientAdminMembershipRoles.has(membership.role))
.map((membership) => membership.clientId)
);
return {
actorId,
clientIds,
isRoot,
snapshot,
};
}
function canAdminManageClient(req, clientId) {
return Boolean(req.nodedcAdminScope?.isRoot || req.nodedcAdminScope?.clientIds.has(clientId));
}
function isProtectedLauncherUser(userId) {
return protectedLauncherUserIds.has(userId);
}
function isProtectedSelfManageRequest(req, userId) {
return isProtectedLauncherUser(userId) && req.nodedcAdminScope?.actorId === userId;
}
function canAdminManageUser(req, userId, options = {}) {
if (isProtectedLauncherUser(userId) && !(options.allowProtectedSelf && isProtectedSelfManageRequest(req, userId))) {
return false;
}
if (req.nodedcAdminScope?.isRoot) {
return true;
}
return req.nodedcAdminScope?.snapshot.data.memberships.some(
(membership) => membership.userId === userId && req.nodedcAdminScope.clientIds.has(membership.clientId)
);
}
function assertAdminCanManageClient(req, res, clientId) {
if (canAdminManageClient(req, clientId)) {
return true;
}
res.status(403).json({ error: "Недостаточно прав для управления этим клиентом" });
return false;
}
function assertAdminCanManageUser(req, res, userId, options = {}) {
if (canAdminManageUser(req, userId, options)) {
return true;
}
res.status(403).json({ error: "Недостаточно прав для управления этим пользователем" });
return false;
}
function assertAdminCanManageAccessForUser(req, res, userId) {
return assertAdminCanManageUser(req, res, userId);
}
function assertAdminCanManageProtectedGroupMembers(req, res, previousMemberIds = [], nextMemberIds = []) {
if (!Array.isArray(nextMemberIds)) {
return true;
}
const previous = new Set(previousMemberIds);
const next = new Set(nextMemberIds);
const changedProtectedUserIds = [...protectedLauncherUserIds].filter((userId) => previous.has(userId) !== next.has(userId));
const blockedUserId = changedProtectedUserIds.find((userId) => !isProtectedSelfManageRequest(req, userId));
if (!blockedUserId) {
return true;
}
res.status(403).json({
error: "protected_user_group_membership_locked",
message: "Защищённого пользователя может добавлять в группы или удалять из групп только он сам.",
});
return false;
}
function assertAdminCanManageMembership(req, res, membership, options = {}) {
if (!assertAdminCanManageClient(req, res, membership.clientId)) {
return false;
}
return assertAdminCanManageUser(req, res, membership.userId, options);
}
function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId, options = {}) {
if (req.nodedcAdminScope?.isRoot) {
if (targetType === "client") {
return true;
}
if (targetType === "user" && (!isProtectedLauncherUser(targetId) || isProtectedSelfManageRequest(req, targetId) || !options.allowProtectedSelf)) {
return true;
}
}
if (targetType === "client") {
return assertAdminCanManageClient(req, res, targetId);
}
if (targetType === "group") {
const group = data.groups.find((candidate) => candidate.id === targetId);
if (!group) {
res.status(404).json({ error: "group_not_found" });
return false;
}
const blockedProtectedUserId = group.memberIds.find((userId) => isProtectedLauncherUser(userId) && !isProtectedSelfManageRequest(req, userId));
if (blockedProtectedUserId) {
res.status(403).json({
error: "protected_group_access_locked",
message: "Гранты группы с защищённым пользователем может менять только сам защищённый пользователь.",
});
return false;
}
return assertAdminCanManageClient(req, res, group.clientId);
}
if (targetType === "user") {
return assertAdminCanManageUser(req, res, targetId, options);
}
res.status(403).json({ error: "Недостаточно прав для управления этим доступом" });
return false;
}
function scopeAdminSnapshot(req, snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user)) {
return {
...snapshot,
data: scopeControlPlaneData(snapshot.data, req.nodedcAdminScope),
};
}
function scopeAdminMutationResult(req, result) {
if (!result?.data) {
return result;
}
return {
...result,
data: scopeControlPlaneData(result.data, req.nodedcAdminScope),
};
}
function scopeSessionControlPlaneData(req, data, actorId) {
const runtimeContext = getRuntimeSessionContext(req.nodedcSession);
const adminScope = resolveAdminScope(runtimeContext.user, runtimeContext.groups);
if (adminScope.isRoot || adminScope.clientIds.size > 0) {
return scopeControlPlaneData(data, adminScope);
}
return scopeRuntimeControlPlaneData(data, actorId);
}
function scopeControlPlaneData(data, scope) {
if (!scope || scope.isRoot) {
return data;
}
const clientIds = scope.clientIds;
const memberships = data.memberships.filter((membership) => clientIds.has(membership.clientId));
const userIds = new Set(memberships.map((membership) => membership.userId));
if (scope.actorId) {
userIds.add(scope.actorId);
}
const groupIds = new Set(data.groups.filter((group) => clientIds.has(group.clientId)).map((group) => group.id));
const userEmails = new Set(
data.users
.filter((user) => userIds.has(user.id))
.map((user) => String(user.email || "").toLowerCase())
.filter(Boolean)
);
return {
...data,
clients: data.clients.filter((client) => clientIds.has(client.id)),
users: data.users.filter((user) => userIds.has(user.id)),
memberships,
groups: data.groups.filter((group) => clientIds.has(group.clientId)),
invites: data.invites.filter((invite) => clientIds.has(invite.clientId)),
accessRequests: [],
taskerInviteRequests: [],
engineWorkflowAccessRequests: data.engineWorkflowAccessRequests.filter((request) =>
userIds.has(request.requesterUserId) ||
userIds.has(request.targetUserId) ||
userEmails.has(String(request.requesterEmail || "").toLowerCase()) ||
userEmails.has(String(request.targetEmail || "").toLowerCase())
),
grants: data.grants.filter((grant) => {
if (grant.targetType === "client") return clientIds.has(grant.targetId);
if (grant.targetType === "group") return groupIds.has(grant.targetId);
if (grant.targetType === "user") return userIds.has(grant.targetId);
return false;
}),
exceptions: data.exceptions.filter((exception) => userIds.has(exception.userId)),
serviceModuleEntitlements: data.serviceModuleEntitlements.filter((entitlement) => userIds.has(entitlement.userId)),
taskManagerMemberships: data.taskManagerMemberships.filter(
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
),
taskManagerProjectMemberships: data.taskManagerProjectMemberships.filter(
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
),
syncStatuses: data.syncStatuses.filter(
(syncStatus) => clientIds.has(syncStatus.objectId) || userIds.has(syncStatus.objectId) || groupIds.has(syncStatus.objectId)
),
auditEvents: data.auditEvents.filter((event) => !event.clientId || clientIds.has(event.clientId)),
};
}
function scopeRuntimeControlPlaneData(data, userId) {
const user = data.users.find((candidate) => candidate.id === userId);
if (!user) {
return {
...data,
clients: [],
users: [],
memberships: [],
groups: [],
invites: [],
accessRequests: [],
revokedAccounts: [],
taskerInviteRequests: [],
engineWorkflowAccessRequests: [],
grants: [],
exceptions: [],
serviceModuleEntitlements: [],
syncStatuses: [],
auditEvents: [],
taskManagerMemberships: [],
taskManagerProjectMemberships: [],
};
}
const memberships = data.memberships.filter((membership) => membership.userId === user.id);
const clientIds = new Set(memberships.map((membership) => membership.clientId));
const groups = data.groups
.filter((group) => clientIds.has(group.clientId) && group.memberIds.includes(user.id))
.map((group) => ({ ...group, memberIds: group.memberIds.filter((memberId) => memberId === user.id) }));
const groupIds = new Set(groups.map((group) => group.id));
const userEmail = String(user.email || "").toLowerCase();
const relatedEngineRequests = data.engineWorkflowAccessRequests.filter((request) =>
request.requesterUserId === user.id ||
request.targetUserId === user.id ||
String(request.requesterEmail || "").toLowerCase() === userEmail ||
String(request.targetEmail || "").toLowerCase() === userEmail
);
return {
...data,
clients: data.clients.filter((client) => clientIds.has(client.id)),
users: [user],
memberships,
groups,
invites: [],
accessRequests: [],
revokedAccounts: [],
taskerInviteRequests: [],
engineWorkflowAccessRequests: relatedEngineRequests,
grants: data.grants.filter((grant) => {
if (grant.targetType === "client") return clientIds.has(grant.targetId);
if (grant.targetType === "group") return groupIds.has(grant.targetId);
if (grant.targetType === "user") return grant.targetId === user.id;
return false;
}),
exceptions: data.exceptions.filter((exception) => exception.userId === user.id),
serviceModuleEntitlements: data.serviceModuleEntitlements.filter((entitlement) => entitlement.userId === user.id),
syncStatuses: [],
auditEvents: [],
taskManagerMemberships: data.taskManagerMemberships.filter(
(membership) => membership.userId === user.id && clientIds.has(membership.clientId)
),
taskManagerProjectMemberships: data.taskManagerProjectMemberships.filter(
(membership) => membership.userId === user.id && clientIds.has(membership.clientId)
),
};
}
function cookieOptions(maxAgeMs) {
const options = {
httpOnly: true,
sameSite: "lax",
secure: config.cookieSecure,
path: "/",
maxAge: maxAgeMs,
};
if (config.cookieDomain) {
options.domain = config.cookieDomain;
}
return options;
}
function clearCookieOptions() {
const options = {
httpOnly: true,
sameSite: "lax",
secure: config.cookieSecure,
path: "/",
};
if (config.cookieDomain) {
options.domain = config.cookieDomain;
}
return options;
}
function shouldDisableHttpCache(req) {
if (req.path.startsWith("/api/") || req.path.startsWith("/auth/")) {
return true;
}
if (req.method !== "GET" && req.method !== "HEAD") {
return false;
}
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
return accept.includes("text/html");
}
function lockNoStoreHeaders(res) {
const setHeader = res.setHeader.bind(res);
setNoStore(res);
res.setHeader = (name, value) => {
const normalizedName = String(name).toLowerCase();
if (normalizedName === "cache-control") {
return setHeader(name, noStoreCacheControl);
}
if (normalizedName === "pragma") {
return setHeader(name, "no-cache");
}
if (normalizedName === "expires") {
return setHeader(name, "0");
}
return setHeader(name, value);
};
}
function setNoStore(res) {
res.setHeader("Cache-Control", noStoreCacheControl);
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
}
function buildLoginRedirectUrl(returnTo, { forceLogin = false, includeReturnTo = false } = {}) {
const loginUrl = new URL("/auth/login", config.appBaseUrl);
const cleanReturnTo = sanitizeReturnTo(returnTo);
if (forceLogin) {
loginUrl.searchParams.set("prompt", "login");
}
if (includeReturnTo || cleanReturnTo !== "/") {
loginUrl.searchParams.set("returnTo", cleanReturnTo);
}
return loginUrl.toString();
}
function buildPlatformInviteUrl(invite) {
return new URL(`/invite/${encodeURIComponent(invite.token)}`, config.appBaseUrl).toString();
}
function resolveInviteRedirectUrl(invite) {
if (invite?.source === "tasker_workspace_invite" && invite.sourceTaskerInviteRequestId) {
return `/tasker-workspace-invite/${encodeURIComponent(invite.sourceTaskerInviteRequestId)}`;
}
return "/";
}
function buildOidcLogoutUrl(discovery, returnTo = "/", idToken = null) {
const issuerUrl = new URL(discovery.issuer || config.issuer);
const logoutUrl = new URL("/if/flow/default-invalidation-flow/", issuerUrl.origin);
logoutUrl.searchParams.set("client_id", config.clientId);
logoutUrl.searchParams.set("post_logout_redirect_uri", buildLoggedOutRedirectUrl(returnTo));
if (idToken) {
logoutUrl.searchParams.set("id_token_hint", idToken);
}
return logoutUrl;
}
function buildLoggedOutRedirectUrl(returnTo = "/") {
const loggedOutUrl = new URL("/auth/logged-out", config.appBaseUrl);
const cleanReturnTo = sanitizeReturnTo(returnTo);
if (cleanReturnTo !== "/") {
loggedOutUrl.searchParams.set("returnTo", cleanReturnTo);
}
return loggedOutUrl.toString();
}
function randomBase64Url(size) {
return randomBytes(size).toString("base64url");
}
function sanitizeReturnTo(returnTo) {
if (typeof returnTo !== "string" || !returnTo.startsWith("/") || returnTo.startsWith("//")) {
return "/";
}
return returnTo;
}
function sanitizePrompt(prompt) {
if (prompt === "login" || prompt === "none" || prompt === "consent" || prompt === "select_account") {
return prompt;
}
return null;
}
function ensureTrailingSlash(value) {
return value.endsWith("/") ? value : `${value}/`;
}
async function sendAdminIdempotentJson(req, res, operation) {
const idempotencyKey = normalizeAdminMutationIdempotencyKey(req);
if (idempotencyKey === false) {
res.status(400).json({
ok: false,
error: "idempotency_key_invalid",
message: "Idempotency-Key must be 8-256 visible characters.",
});
return;
}
if (!idempotencyKey) {
res.json(await operation());
return;
}
pruneAdminMutationIdempotencyRecords();
const recordKey = buildAdminMutationIdempotencyRecordKey(req, idempotencyKey);
const fingerprint = buildAdminMutationFingerprint(req);
const existingRecord = adminMutationIdempotencyRecords.get(recordKey);
if (existingRecord && existingRecord.fingerprint !== fingerprint) {
res.status(409).json({
ok: false,
error: "idempotency_key_conflict",
message: "Idempotency-Key was already used for a different admin mutation.",
});
return;
}
if (existingRecord?.status === "completed") {
res.setHeader("Idempotency-Key", idempotencyKey);
res.setHeader("Idempotency-Replayed", "true");
res.status(existingRecord.statusCode).json(existingRecord.body);
return;
}
if (existingRecord?.status === "pending") {
const response = await existingRecord.promise;
res.setHeader("Idempotency-Key", idempotencyKey);
res.setHeader("Idempotency-Replayed", "true");
res.status(response.statusCode).json(response.body);
return;
}
const promise = Promise.resolve(operation()).then((body) => ({ statusCode: 200, body }));
adminMutationIdempotencyRecords.set(recordKey, {
status: "pending",
createdAt: Date.now(),
fingerprint,
promise,
});
try {
const response = await promise;
adminMutationIdempotencyRecords.set(recordKey, {
status: "completed",
createdAt: Date.now(),
fingerprint,
statusCode: response.statusCode,
body: response.body,
});
res.setHeader("Idempotency-Key", idempotencyKey);
res.status(response.statusCode).json(response.body);
} catch (error) {
adminMutationIdempotencyRecords.delete(recordKey);
throw error;
}
}
function normalizeAdminMutationIdempotencyKey(req) {
const rawHeader = req.headers["idempotency-key"];
const rawValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
if (rawValue === undefined || rawValue === null || rawValue === "") {
return null;
}
const value = String(rawValue).trim();
if (!/^[\x21-\x7E]{8,256}$/.test(value)) {
return false;
}
return value;
}
function buildAdminMutationIdempotencyRecordKey(req, idempotencyKey) {
const actorId =
req.nodedcAdminScope?.actorId ||
req.nodedcSession?.user?.sub ||
req.nodedcSession?.user?.email ||
"unknown";
return `${actorId}:${idempotencyKey}`;
}
function buildAdminMutationFingerprint(req) {
const routePath = req.route?.path || req.path;
const bodyHash = createHash("sha256").update(stableJson(req.body ?? null)).digest("hex");
return `${req.method}:${routePath}:${req.path}:${bodyHash}`;
}
function pruneAdminMutationIdempotencyRecords() {
const expiresBefore = Date.now() - adminMutationIdempotencyTtlMs;
for (const [key, record] of adminMutationIdempotencyRecords.entries()) {
if ((record.createdAt ?? 0) < expiresBefore) {
adminMutationIdempotencyRecords.delete(key);
}
}
}
function stableJson(value) {
if (Array.isArray(value)) {
return `[${value.map((item) => stableJson(item)).join(",")}]`;
}
if (value && typeof value === "object") {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
function asyncRoute(handler) {
return (req, res, next) => {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
function loadEnvFiles(candidates) {
for (const candidate of candidates) {
if (!candidate) continue;
const envPath = resolve(projectRoot, candidate);
if (!existsSync(envPath)) continue;
const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
const separatorIndex = trimmed.indexOf("=");
const key = trimmed.slice(0, separatorIndex).trim();
const value = stripEnvQuotes(trimmed.slice(separatorIndex + 1).trim());
if (!process.env[key]) {
process.env[key] = value;
}
}
}
}
function stripEnvQuotes(value) {
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
return value;
}