import { resolveAssistantAction } from '../assistant-action-resolver.mjs' const HUB_ACTIONS = new Set([ 'hub.user.read_admin_summary', 'hub.invite.list_pending', 'hub.access_request.list_pending', 'hub.user.block', 'hub.user.unblock', 'hub.membership.change_role', 'hub.membership.disable', 'hub.assistant_access.change_role', ]) const MEMBERSHIP_ROLES = new Set(['client_owner', 'client_admin', 'member']) const CORE_ASSISTANT_ROLES = new Set(['blocked', 'member', 'admin']) function requireValue(input, key) { const value = input[key] if (value === undefined || value === null || String(value).trim() === '') { throw new Error(`Missing required adapter input: ${key}`) } return String(value) } function encode(value) { return encodeURIComponent(String(value)) } function makeIdempotencyKey(actionId, input) { if (input.idempotencyKey) return String(input.idempotencyKey) return [ actionId, input.actorUserId || 'actor_unknown', input.targetUserId || input.membershipId || 'target_unknown', input.targetRole || input.targetAssistantRole || input.targetStatus || 'default', ].join(':') } function patchPlan(actionId, input, path, body, summary) { return { method: 'PATCH', path, body, headers: { 'Content-Type': 'application/json', 'Idempotency-Key': makeIdempotencyKey(actionId, input), }, summary, } } function buildPlanForAction(actionId, input) { switch (actionId) { case 'hub.user.read_admin_summary': return { method: 'GET', path: '/api/admin/control-plane', query: { projection: 'users,memberships,services,grants,exceptions,invites,accessRequests', clientId: input.clientId || null, }, summary: 'Read admin-visible Launcher control-plane summary and filter in adapter output.', } case 'hub.invite.list_pending': return { method: 'GET', path: '/api/admin/control-plane', query: { projection: 'invites', status: 'pending', clientId: input.clientId || null, }, summary: 'Read Launcher invites and return pending invites visible in current admin scope.', } case 'hub.access_request.list_pending': return { method: 'GET', path: '/api/admin/control-plane', query: { projection: 'accessRequests,engineWorkflowAccessRequests,users,clients', status: 'pending', clientId: input.clientId || null, }, summary: 'Read Launcher access requests and return pending requests visible in current admin scope.', } case 'hub.user.block': return patchPlan( actionId, input, `/api/admin/users/${encode(requireValue(input, 'targetUserId'))}/profile`, { globalStatus: 'blocked' }, 'Block Launcher user via guarded profile update route.', ) case 'hub.user.unblock': return patchPlan( actionId, input, `/api/admin/users/${encode(requireValue(input, 'targetUserId'))}/profile`, { globalStatus: 'active' }, 'Unblock Launcher user via guarded profile update route.', ) case 'hub.membership.change_role': { const targetRole = requireValue(input, 'targetRole') if (!MEMBERSHIP_ROLES.has(targetRole)) { throw new Error(`Invalid targetRole: ${targetRole}`) } return patchPlan( actionId, input, `/api/admin/memberships/${encode(requireValue(input, 'membershipId'))}`, { role: targetRole }, 'Change Launcher client membership role via guarded membership update route.', ) } case 'hub.membership.disable': return patchPlan( actionId, input, `/api/admin/memberships/${encode(requireValue(input, 'membershipId'))}`, { status: 'disabled' }, 'Disable Launcher client membership via guarded membership update route.', ) case 'hub.assistant_access.change_role': { const targetAssistantRole = requireValue(input, 'targetAssistantRole') if (!CORE_ASSISTANT_ROLES.has(targetAssistantRole)) { throw new Error(`Invalid targetAssistantRole: ${targetAssistantRole}`) } return patchPlan( actionId, input, `/api/admin/memberships/${encode(requireValue(input, 'membershipId'))}`, { coreAssistantRole: targetAssistantRole }, 'Change NDC Core Assistant access role via guarded membership update route.', ) } default: throw new Error(`Unsupported HUB action: ${actionId}`) } } export async function buildHubLauncherAdminPlan(input = {}, options = {}) { const decision = options.decision || await resolveAssistantAction(input, options) const actionId = decision.action?.id if (!actionId) { return { ok: false, decision: 'unsupported_hub_action', reason: 'Input did not resolve to a supported HUB Launcher admin action.', actionDecision: decision, } } if ( decision.decision !== 'allowed' && !(options.planPreview === true && decision.decision === 'needs_confirmation') ) { return { ok: false, decision: decision.decision, reason: decision.reason, actionDecision: decision, } } if (!HUB_ACTIONS.has(actionId)) { return { ok: false, decision: 'unsupported_hub_action', reason: 'Input did not resolve to a supported HUB Launcher admin action.', actionDecision: decision, } } const plan = buildPlanForAction(actionId, input) const adapterReady = decision.action.adapterStatus === 'implemented' return { ok: true, decision: 'planned', adapterId: 'adapter.hub.launcher_admin_api', adapterStatus: decision.action.adapterStatus, executionReady: adapterReady, dryRunOnly: !adapterReady, gatewayActor: { userId: input.actorUserId || null, email: input.actorEmail || null, subject: input.actorSubject || null, }, safety: { noDeleteRouteMapped: true, requiresExternalExecutor: true, sourceGuardedRoutesOnly: true, confirmationAlreadyChecked: decision.execution.confirmed || options.planPreview === true, }, request: plan, actionDecision: decision, } }