200 lines
6.2 KiB
JavaScript
200 lines
6.2 KiB
JavaScript
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,
|
|
}
|
|
}
|