NODEDC_PLATFORM/services/ontology-core/src/adapters/hub-launcher-admin.mjs

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,
}
}