From 71c62579dc8b3f07c0a1075eee221aa14373d6ae Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 23 Jun 2026 16:19:22 +0300 Subject: [PATCH] Expose Launcher session validation for service handoff --- server/dev-server.mjs | 81 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/server/dev-server.mjs b/server/dev-server.mjs index 001d9db..b272697 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -436,7 +436,7 @@ app.get("/api/services/:serviceSlug/optional-launch", (req, res) => { return; } - redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/"); + redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/", session); }); app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => { @@ -449,7 +449,7 @@ app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => { return; } - redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/"); + redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/", req.nodedcSession); }); app.post("/api/internal/handoff/consume", (req, res) => { @@ -486,6 +486,7 @@ app.post("/api/internal/handoff/consume", (req, res) => { res.json({ ok: true, serviceSlug, + launcherSessionId: handoff.launcherSessionId ?? null, user: { id: user.id, email: user.email, @@ -498,6 +499,69 @@ app.post("/api/internal/handoff/consume", (req, res) => { }); }); +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); @@ -1111,7 +1175,7 @@ app.get("/tasker-workspace-invite/:taskerInviteRequestId", (req, res) => { return; } - const handoffToken = createServiceHandoff("task-manager", runtimeContext.user); + 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); @@ -3147,9 +3211,9 @@ function buildOptionalServiceFallbackUrl(serviceSlug, app, fallbackPath) { } } -function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, returnTo = "/") { +function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, returnTo = "/", session = null) { if (isEngineServiceSlug(serviceSlug)) { - const handoffToken = createServiceHandoff("nodedc", runtimeContext.user); + const handoffToken = createServiceHandoff("nodedc", runtimeContext.user, { launcherSessionId: session?.id }); const targetUrl = new URL("/auth/nodedc/handoff/", getEngineLaunchTargetUrl(app)); const nextPath = sanitizeReturnTo(returnTo); @@ -3164,7 +3228,7 @@ function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, re } if (isBimViewerServiceSlug(serviceSlug)) { - const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user); + const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id }); const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app)); const nextPath = sanitizeReturnTo(returnTo); @@ -3183,7 +3247,7 @@ function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, re return; } - const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user); + const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id }); const targetUrl = new URL("/auth/nodedc/handoff/", getTaskBaseUrl()); const nextPath = sanitizeReturnTo(returnTo); @@ -3626,12 +3690,13 @@ function resolveTaskManagerWorkspaceAssignments(data, user) { return [...bySlug.values()]; } -function createServiceHandoff(serviceSlug, user) { +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,