Compare commits
No commits in common. "ai-workspaces-npm" and "main" have entirely different histories.
ai-workspa
...
main
|
|
@ -205,14 +205,6 @@ export function resolveRequiredGroups(data, user) {
|
|||
groupNames.add(platformGroups.taskManagerAdmin);
|
||||
groupNames.add(platformGroups.taskManagerUser);
|
||||
addGroups(groupNames, resolveEngineRoleGroups("member"));
|
||||
// The platform super-admin is the default owner of every internally
|
||||
// registered service. This also keeps the Authentik view consistent with
|
||||
// the Launcher control-plane grants (including Module Foundry).
|
||||
for (const service of data.services) {
|
||||
if (service.authentikGroupName) {
|
||||
groupNames.add(service.authentikGroupName);
|
||||
}
|
||||
}
|
||||
return [...groupNames];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,40 +107,6 @@ const bimViewerDefaultGrants = [
|
|||
updatedAt: "2026-06-18T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
const moduleFoundryService = {
|
||||
id: "service_module_foundry",
|
||||
slug: "module-foundry",
|
||||
title: "MODULE FOUNDRY",
|
||||
subtitle: "Конфигурация платформенных модулей",
|
||||
description: "Сборка приложений NODE.DC из утверждённых страниц, визуальных профилей и онтологических сущностей.",
|
||||
fullDescription:
|
||||
"Module Foundry — внутренний контур для конфигурации модулей NODE.DC. В первой версии доступ открывается только через Launcher; самостоятельной регистрации, публичного входа и многопользовательского владения проектами нет.",
|
||||
url: "https://foundry.nodedc.ru",
|
||||
launchUrl: "https://foundry.nodedc.ru",
|
||||
logoutUrl: "https://foundry.nodedc.ru/auth/logout",
|
||||
accentColor: "#F12F92",
|
||||
fallbackGradient: "linear-gradient(132deg, rgba(241, 47, 146, 0.8), rgba(67, 19, 58, 0.92) 48%, #080B0F 86%)",
|
||||
status: "active",
|
||||
order: 26,
|
||||
authentikApplicationSlug: "module-foundry",
|
||||
authentikGroupName: "nodedc:module-foundry:access",
|
||||
authHandoffPath: "/auth/nodedc/handoff",
|
||||
isAvailableForAllNewClients: false,
|
||||
createdAt: "2026-07-14T00:00:00.000Z",
|
||||
updatedAt: "2026-07-14T00:00:00.000Z",
|
||||
};
|
||||
const moduleFoundryDefaultGrants = [
|
||||
{
|
||||
id: "grant_module_foundry_dctouch_admins",
|
||||
serviceId: "service_module_foundry",
|
||||
targetType: "group",
|
||||
targetId: "group_dctouch_admins",
|
||||
appRole: "member",
|
||||
status: "active",
|
||||
createdAt: "2026-07-14T00:00:00.000Z",
|
||||
updatedAt: "2026-07-14T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
const defaultSettings = {
|
||||
brand: {
|
||||
logoLinkUrl: "/",
|
||||
|
|
@ -2367,7 +2333,6 @@ function normalizeData(payload) {
|
|||
data.engineWorkflowAccessRequests = data.engineWorkflowAccessRequests.map(normalizeEngineWorkflowAccessRequest).filter(Boolean);
|
||||
applyProtectedLauncherUserInvariants(data);
|
||||
applyBimViewerServiceInvariants(data);
|
||||
applyModuleFoundryServiceInvariants(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -2431,65 +2396,6 @@ function applyBimViewerServiceInvariants(data) {
|
|||
}
|
||||
}
|
||||
|
||||
function applyModuleFoundryServiceInvariants(data) {
|
||||
let service = data.services.find(
|
||||
(candidate) => candidate.id === moduleFoundryService.id || candidate.slug === moduleFoundryService.slug
|
||||
);
|
||||
|
||||
if (!service) {
|
||||
service = normalizeService({ ...moduleFoundryService });
|
||||
data.services.push(service);
|
||||
} else {
|
||||
service.id = service.id || moduleFoundryService.id;
|
||||
service.slug = service.slug || moduleFoundryService.slug;
|
||||
service.title = service.title || moduleFoundryService.title;
|
||||
service.subtitle = service.subtitle || moduleFoundryService.subtitle;
|
||||
service.description = service.description || moduleFoundryService.description;
|
||||
service.fullDescription = service.fullDescription || moduleFoundryService.fullDescription;
|
||||
service.url = service.url || moduleFoundryService.url;
|
||||
service.launchUrl = service.launchUrl || service.url || moduleFoundryService.launchUrl;
|
||||
service.logoutUrl = service.logoutUrl || moduleFoundryService.logoutUrl;
|
||||
service.accentColor = service.accentColor || moduleFoundryService.accentColor;
|
||||
service.fallbackGradient = service.fallbackGradient || moduleFoundryService.fallbackGradient;
|
||||
service.status = service.status || moduleFoundryService.status;
|
||||
service.order = service.order ?? moduleFoundryService.order;
|
||||
service.authentikApplicationSlug = service.authentikApplicationSlug || moduleFoundryService.authentikApplicationSlug;
|
||||
service.authentikGroupName = service.authentikGroupName || moduleFoundryService.authentikGroupName;
|
||||
service.authHandoffPath = service.authHandoffPath || moduleFoundryService.authHandoffPath;
|
||||
service.isAvailableForAllNewClients = service.isAvailableForAllNewClients ?? false;
|
||||
service.createdAt = service.createdAt || moduleFoundryService.createdAt;
|
||||
service.updatedAt = service.updatedAt || moduleFoundryService.updatedAt;
|
||||
}
|
||||
|
||||
const serviceId = service.id || moduleFoundryService.id;
|
||||
const defaultGrant = { ...moduleFoundryDefaultGrants[0], serviceId };
|
||||
const groupExists = data.groups.some((group) => group.id === defaultGrant.targetId);
|
||||
const grantExists = data.grants.some(
|
||||
(candidate) =>
|
||||
candidate.serviceId === defaultGrant.serviceId &&
|
||||
candidate.targetType === defaultGrant.targetType &&
|
||||
candidate.targetId === defaultGrant.targetId
|
||||
);
|
||||
if (groupExists && !grantExists) {
|
||||
data.grants.push(defaultGrant);
|
||||
}
|
||||
|
||||
ensureStaticSyncStatus(data, {
|
||||
id: "sync_module_foundry_authentik",
|
||||
objectId: serviceId,
|
||||
objectName: moduleFoundryService.title,
|
||||
objectType: "service",
|
||||
});
|
||||
if (groupExists) {
|
||||
ensureStaticSyncStatus(data, {
|
||||
id: `sync_${defaultGrant.id}`,
|
||||
objectId: defaultGrant.id,
|
||||
objectName: `${moduleFoundryService.slug}:${defaultGrant.targetType}:${defaultGrant.targetId}`,
|
||||
objectType: "grant",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStaticSyncStatus(data, status) {
|
||||
const exists = data.syncStatuses.some(
|
||||
(candidate) => candidate.target === "authentik" && candidate.objectType === status.objectType && candidate.objectId === status.objectId
|
||||
|
|
@ -2585,22 +2491,6 @@ function upsertSystemProtectedUserGrant(data, user, service, appRole, now) {
|
|||
|
||||
function normalizeService(service) {
|
||||
if (typeof service !== "object" || service === null) return service;
|
||||
if (
|
||||
service.id === "service_module_foundry" ||
|
||||
service.slug === "module-foundry" ||
|
||||
service.authentikApplicationSlug === "module-foundry"
|
||||
) {
|
||||
return {
|
||||
...service,
|
||||
url: service.url || moduleFoundryService.url,
|
||||
launchUrl: service.launchUrl || service.url || moduleFoundryService.launchUrl,
|
||||
logoutUrl: service.logoutUrl || moduleFoundryService.logoutUrl,
|
||||
authentikApplicationSlug: service.authentikApplicationSlug || moduleFoundryService.authentikApplicationSlug,
|
||||
authentikGroupName: service.authentikGroupName || moduleFoundryService.authentikGroupName,
|
||||
authHandoffPath: service.authHandoffPath || moduleFoundryService.authHandoffPath,
|
||||
isAvailableForAllNewClients: service.isAvailableForAllNewClients ?? false,
|
||||
};
|
||||
}
|
||||
if (service.id === "service_bim_viewer" || service.slug === "bim-viewer" || service.authentikApplicationSlug === "bim-viewer") {
|
||||
const legacyLocalUrl = (value) => value === "http://localhost:8080" || value === "http://127.0.0.1:8080";
|
||||
return {
|
||||
|
|
@ -3790,7 +3680,6 @@ function sanitizeServicePatch(payload, service) {
|
|||
"fallbackGradient",
|
||||
"authentikApplicationSlug",
|
||||
"authentikGroupName",
|
||||
"authHandoffPath",
|
||||
];
|
||||
|
||||
for (const field of stringFields) {
|
||||
|
|
|
|||
|
|
@ -674,62 +674,6 @@ app.post("/api/internal/access/check", (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
app.post("/api/ai-workspace/internal/v1/entitlements", (req, res) => {
|
||||
if (!isInternalRequestAuthorized(req)) {
|
||||
res.status(config.internalAccessToken ? 401 : 503).json({
|
||||
ok: false,
|
||||
error: config.internalAccessToken ? "ai_workspace_entitlement_unauthorized" : "internal_access_not_configured",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = controlPlaneStore.getSnapshot({ name: "AI Workspace entitlement adapter", source: "ai-workspace" });
|
||||
const ownerLookup = aiWorkspaceEntitlementOwnerLookup(req.body);
|
||||
const user = findInternalAccessUser(snapshot.data, ownerLookup);
|
||||
const requestedAt = new Date().toISOString();
|
||||
|
||||
if (!user) {
|
||||
res.json({
|
||||
ok: true,
|
||||
schemaVersion: "ai-workspace.entitlement-response.v1",
|
||||
source: "launcher-control-plane",
|
||||
requestedAt,
|
||||
appGrants: buildAiWorkspaceDeniedAppGrants("launcher_user_not_found", requestedAt),
|
||||
user: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = resolveRequiredGroups(snapshot.data, user);
|
||||
const apps = getAppsForUser(groups);
|
||||
const adminScope = resolveAdminScope(buildAssistantGatewaySessionUser(user, groups), groups);
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
schemaVersion: "ai-workspace.entitlement-response.v1",
|
||||
source: "launcher-control-plane",
|
||||
requestedAt,
|
||||
appGrants: buildAiWorkspaceAppGrants({
|
||||
data: snapshot.data,
|
||||
user,
|
||||
groups,
|
||||
apps,
|
||||
adminScope,
|
||||
payload: req.body,
|
||||
requestedAt,
|
||||
}),
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl ?? null,
|
||||
authentikUserId: user.authentikUserId ?? null,
|
||||
globalStatus: user.globalStatus,
|
||||
groups,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/internal/tasker/invite-requests", asyncRoute(async (req, res) => {
|
||||
if (!isInternalRequestAuthorized(req)) {
|
||||
res.status(config.internalAccessToken ? 401 : 503).json({
|
||||
|
|
@ -3108,207 +3052,6 @@ function getSessionAccessState(session) {
|
|||
}
|
||||
}
|
||||
|
||||
function aiWorkspaceEntitlementOwnerLookup(payload) {
|
||||
const source = isPlainRecord(payload) ? payload : {};
|
||||
const owner = isPlainRecord(source.owner) ? source.owner : {};
|
||||
const ownerKey = normalizeOptionalText(owner.ownerKey ?? owner.owner_key ?? source.ownerKey ?? source.owner_key);
|
||||
const ownerKeyParts = parseAiWorkspaceOwnerKey(ownerKey);
|
||||
|
||||
return {
|
||||
userId: normalizeOptionalText(owner.userId ?? owner.user_id ?? source.userId ?? source.user_id) ?? ownerKeyParts.userId,
|
||||
email: normalizeOptionalText(owner.email ?? source.email) ?? ownerKeyParts.email,
|
||||
subject: normalizeOptionalText(owner.subject ?? owner.sub ?? source.subject ?? source.sub) ?? ownerKeyParts.subject,
|
||||
};
|
||||
}
|
||||
|
||||
function parseAiWorkspaceOwnerKey(ownerKey) {
|
||||
const text = normalizeOptionalText(ownerKey) ?? "";
|
||||
if (text.startsWith("email:")) return { email: text.slice("email:".length), userId: null, subject: null };
|
||||
if (text.startsWith("user:")) return { email: null, userId: text.slice("user:".length), subject: null };
|
||||
return { email: null, userId: null, subject: text || null };
|
||||
}
|
||||
|
||||
function buildAiWorkspaceDeniedAppGrants(reason, requestedAt) {
|
||||
return {
|
||||
launcher: aiWorkspaceGrant({
|
||||
appId: "launcher",
|
||||
appTitle: "NODE.DC Launcher",
|
||||
allowed: false,
|
||||
reason,
|
||||
requestedAt,
|
||||
}),
|
||||
engine: aiWorkspaceGrant({
|
||||
appId: "engine",
|
||||
appTitle: "NODE.DC Engine / InJoin",
|
||||
allowed: false,
|
||||
reason,
|
||||
requestedAt,
|
||||
}),
|
||||
ops: aiWorkspaceGrant({
|
||||
appId: "ops",
|
||||
appTitle: "NODE.DC Ops / Tasker",
|
||||
allowed: false,
|
||||
reason,
|
||||
requestedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAiWorkspaceAppGrants({ data, user, groups, apps, adminScope, payload, requestedAt }) {
|
||||
const launcherApp = apps.find((candidate) => candidate.slug === "launcher");
|
||||
const opsApp = apps.find((candidate) => candidate.slug === "task-manager");
|
||||
const engineApp = apps.find((candidate) => isEngineServiceSlug(candidate.slug));
|
||||
const membership = resolveAiWorkspaceBestMembership(data, user);
|
||||
const opsWorkspaceSlug = resolveAiWorkspaceOpsWorkspaceSlug(payload);
|
||||
const opsServiceModules = aiWorkspaceAppAllowed(user, opsApp)
|
||||
? resolveTaskManagerWorkspaceServiceModules(data, user, "task-manager", opsWorkspaceSlug)
|
||||
: {};
|
||||
|
||||
const baseContext = {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
launcherUserStatus: user.globalStatus ?? "active",
|
||||
groups,
|
||||
membershipRole: membership?.role ?? null,
|
||||
membershipStatus: membership?.status ?? null,
|
||||
coreAssistantRole: membership?.coreAssistantRole ?? null,
|
||||
};
|
||||
const adminContext = {
|
||||
...baseContext,
|
||||
adminScope: adminScope?.isRoot
|
||||
? "root"
|
||||
: adminScope?.clientIds?.size
|
||||
? "client"
|
||||
: "none",
|
||||
adminClientIds: adminScope?.clientIds ? Array.from(adminScope.clientIds).sort() : [],
|
||||
};
|
||||
|
||||
return {
|
||||
launcher: aiWorkspaceGrant({
|
||||
appId: "launcher",
|
||||
appTitle: "NODE.DC Launcher",
|
||||
surface: "launcher",
|
||||
allowed: aiWorkspaceAppAllowed(user, launcherApp),
|
||||
reason: aiWorkspaceAccessReason(user, launcherApp),
|
||||
scopes: aiWorkspaceLauncherScopes(adminScope),
|
||||
context: adminContext,
|
||||
requestedAt,
|
||||
}),
|
||||
engine: aiWorkspaceGrant({
|
||||
appId: "engine",
|
||||
appTitle: "NODE.DC Engine / InJoin",
|
||||
surface: "engine",
|
||||
allowed: aiWorkspaceAppAllowed(user, engineApp),
|
||||
reason: aiWorkspaceAccessReason(user, engineApp),
|
||||
scopes: ["engine:workspace:read", "engine:workflow:read"],
|
||||
context: {
|
||||
...baseContext,
|
||||
serviceSlug: engineApp?.slug ?? null,
|
||||
matchedGroups: engineApp?.matchedGroups ?? [],
|
||||
},
|
||||
requestedAt,
|
||||
}),
|
||||
ops: aiWorkspaceGrant({
|
||||
appId: "ops",
|
||||
appTitle: "NODE.DC Ops / Tasker",
|
||||
surface: "ops",
|
||||
allowed: aiWorkspaceAppAllowed(user, opsApp),
|
||||
reason: aiWorkspaceAccessReason(user, opsApp),
|
||||
scopes: [
|
||||
"ops:project:read",
|
||||
"ops:card:read",
|
||||
"ops:card:create",
|
||||
...(opsServiceModules.codex_agents ? ["ops:codex-agent:use"] : []),
|
||||
],
|
||||
context: {
|
||||
...baseContext,
|
||||
serviceSlug: opsApp?.slug ?? null,
|
||||
opsWorkspaceSlug,
|
||||
matchedGroups: opsApp?.matchedGroups ?? [],
|
||||
serviceModules: opsServiceModules,
|
||||
},
|
||||
requestedAt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function aiWorkspaceGrant({ appId, appTitle, surface, allowed, reason, scopes = [], context = {}, requestedAt }) {
|
||||
return {
|
||||
appId,
|
||||
appTitle,
|
||||
surface: surface ?? appId,
|
||||
status: allowed ? "granted" : "denied",
|
||||
granted: Boolean(allowed),
|
||||
allowed: Boolean(allowed),
|
||||
enabled: Boolean(allowed),
|
||||
denied: !allowed,
|
||||
reason: allowed ? "launcher_access_confirmed" : reason,
|
||||
deniedReason: allowed ? null : reason,
|
||||
deniedText: allowed ? null : "Доступ к модулю ограничен, обратитесь к администратору системы.",
|
||||
scopes: allowed ? scopes : [],
|
||||
context,
|
||||
updatedAt: requestedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function aiWorkspaceAppAllowed(user, app) {
|
||||
return user?.globalStatus === "active" && Boolean(app?.hasAccess) && (app.status ?? "disabled") === "active";
|
||||
}
|
||||
|
||||
function aiWorkspaceAccessReason(user, app) {
|
||||
if (!user) return "launcher_user_not_found";
|
||||
if (user.globalStatus !== "active") return "launcher_user_blocked";
|
||||
if (!app) return "launcher_service_not_found";
|
||||
if (!app.hasAccess) return "launcher_service_access_denied";
|
||||
if ((app.status ?? "disabled") !== "active") return "launcher_service_inactive";
|
||||
return "launcher_access_confirmed";
|
||||
}
|
||||
|
||||
function aiWorkspaceLauncherScopes(adminScope) {
|
||||
const scopes = ["launcher:access:read", "assistant:use"];
|
||||
if (adminScope?.isRoot || adminScope?.clientIds?.size) {
|
||||
scopes.push(
|
||||
"launcher_admin_scope.any",
|
||||
"launcher_admin_scope.can_manage_user",
|
||||
"launcher_admin_scope.can_manage_membership",
|
||||
"launcher:users:read",
|
||||
"launcher:roles:read"
|
||||
);
|
||||
}
|
||||
if (adminScope?.isRoot) {
|
||||
scopes.push("launcher_admin_scope.root", "launcher:service-catalog:manage");
|
||||
}
|
||||
return [...new Set(scopes)].sort();
|
||||
}
|
||||
|
||||
function resolveAiWorkspaceBestMembership(data, user) {
|
||||
if (!user?.id) return null;
|
||||
return data.memberships
|
||||
.filter((membership) => membership.userId === user.id && membership.status === "active")
|
||||
.slice()
|
||||
.sort((left, right) => assistantMembershipWeight(right) - assistantMembershipWeight(left))[0] ?? null;
|
||||
}
|
||||
|
||||
function resolveAiWorkspaceOpsWorkspaceSlug(payload) {
|
||||
const source = isPlainRecord(payload) ? payload : {};
|
||||
const activeContext = isPlainRecord(source.activeContext) ? source.activeContext : {};
|
||||
const runContext = isPlainRecord(source.runContext) ? source.runContext : {};
|
||||
const activeOps = isPlainRecord(activeContext.ops) ? activeContext.ops : {};
|
||||
const runOps = isPlainRecord(runContext.contexts?.ops)
|
||||
? runContext.contexts.ops
|
||||
: isPlainRecord(runContext.ops)
|
||||
? runContext.ops
|
||||
: {};
|
||||
return normalizeOptionalText(
|
||||
runOps.opsWorkspaceSlug ??
|
||||
runOps.workspaceSlug ??
|
||||
activeOps.opsWorkspaceSlug ??
|
||||
activeOps.workspaceSlug ??
|
||||
source.opsWorkspaceSlug ??
|
||||
source.workspaceSlug
|
||||
);
|
||||
}
|
||||
|
||||
function getAppsForUser(userGroups) {
|
||||
const groupSet = new Set(userGroups);
|
||||
const catalog = getAppCatalog();
|
||||
|
|
@ -3347,7 +3090,6 @@ function getAppCatalog() {
|
|||
url: getServiceUrl(service),
|
||||
openUrl: getServiceUrl(service),
|
||||
launchTargetUrl: getServiceLaunchTargetUrl(service),
|
||||
authHandoffPath: service.authHandoffPath || null,
|
||||
status: service.status ?? "disabled",
|
||||
provider: "authentik",
|
||||
requiredGroups,
|
||||
|
|
@ -3406,12 +3148,7 @@ function specialRequiredGroups(serviceOrSlug) {
|
|||
}
|
||||
|
||||
function getServiceUrl(service) {
|
||||
if (
|
||||
service.slug === "task-manager" ||
|
||||
isEngineServiceSlug(service.slug) ||
|
||||
isBimViewerServiceSlug(service.slug) ||
|
||||
service.authHandoffPath
|
||||
) {
|
||||
if (service.slug === "task-manager" || isEngineServiceSlug(service.slug) || isBimViewerServiceSlug(service.slug)) {
|
||||
return `/api/services/${encodeURIComponent(service.slug)}/launch`;
|
||||
}
|
||||
|
||||
|
|
@ -3419,14 +3156,6 @@ function getServiceUrl(service) {
|
|||
}
|
||||
|
||||
function getServiceLaunchTargetUrl(service) {
|
||||
// Catalog entries replace `url` with the Launcher launch route for services
|
||||
// that need a handoff. Keep their external origin separately so a relative
|
||||
// handoff path is always resolved against the target application, never the
|
||||
// internal `/api/services/:slug/launch` route.
|
||||
if (service?.launchTargetUrl) {
|
||||
return service.launchTargetUrl;
|
||||
}
|
||||
|
||||
if (isEngineServiceSlug(service?.slug)) {
|
||||
return service.launchUrl || service.url || getEnginePublicBaseUrl();
|
||||
}
|
||||
|
|
@ -3514,25 +3243,6 @@ function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, re
|
|||
return;
|
||||
}
|
||||
|
||||
// Module Foundry and future internal services use the same Launcher-owned
|
||||
// session contract as BIM. The path is service metadata, so the launcher
|
||||
// does not grow a separate authentication mechanism per module.
|
||||
const handoffPath = String(app?.authHandoffPath || "").trim();
|
||||
if (handoffPath) {
|
||||
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id });
|
||||
const targetUrl = new URL(handoffPath, getServiceLaunchTargetUrl(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;
|
||||
|
|
|
|||
|
|
@ -31,11 +31,6 @@ export interface Service {
|
|||
order: number;
|
||||
authentikApplicationSlug?: string | null;
|
||||
authentikGroupName?: string | null;
|
||||
/**
|
||||
* Relative handoff endpoint on the target app. Services using the shared
|
||||
* Launcher/Authentik session path never expose a standalone browser token.
|
||||
*/
|
||||
authHandoffPath?: string | null;
|
||||
isAvailableForAllNewClients?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
|
|
|||
|
|
@ -49,16 +49,6 @@ describe("assistant-ready Launcher admin routes", () => {
|
|||
expect(builderBlock).toContain("hasAlternativeAssistantAdminPath");
|
||||
});
|
||||
|
||||
it("exposes AI Workspace entitlements only through internal auth", () => {
|
||||
const block = sourceBlock('app.post("/api/ai-workspace/internal/v1/entitlements"', 'app.post("/api/internal/tasker/invite-requests"');
|
||||
|
||||
expect(block).toContain("isInternalRequestAuthorized(req)");
|
||||
expect(block).toContain("aiWorkspaceEntitlementOwnerLookup(req.body)");
|
||||
expect(block).toContain("buildAiWorkspaceAppGrants");
|
||||
expect(block).toContain('schemaVersion: "ai-workspace.entitlement-response.v1"');
|
||||
expect(block).toContain("appGrants");
|
||||
});
|
||||
|
||||
it("keeps user profile mutation guarded, idempotent, audited, and scoped", () => {
|
||||
const block = sourceBlock('app.patch("/api/admin/users/:userId/profile"', 'app.delete("/api/admin/users/:userId"');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue