/**
* mcp/skills/requestIdemeumIdpReset.ts — request_idemeum_idp_reset
*
* Fallback password-reset path: when the IDP's self-service portal is
* disabled (or the user tried and failed), the agent POSTs to idemeum
* cloud, which holds pre-configured admin-delegated IDP credentials
* (Okta admin API tokens, Entra graph permissions, Google Workspace
* admin SDK) and invokes the IDP's admin API to trigger the reset.
*
* The agent never sees IDP admin credentials — those live only on the
* idemeum cloud service. The agent's role is to forward context
* (agentId, username, idp, tenant, platform) and surface the response.
*
* Wire contract
* -------------
* POST ${IDEMEUM_IDP_URL}/v1/password-reset
* Authorization: Bearer ${IDEMEUM_IDP_API_KEY}
* Body: { agentId, username, idp, tenant, platform }
*
* Success → { status: "initiated", deliveryMethod, message, ticketId? }
* Failure → { status: "failed", message }
* Refused → { status: "not-eligible", message }
*
* When IDEMEUM_IDP_URL is unset, the tool resolves fail-open with
* { status: "not-configured", message: "…" } so the skill prose can
* still branch gracefully.
*
* Guardrail declarations (see docs/skills/SKILL-ROADMAP.md § Guardrail table):
* riskLevel: "high" — identity-level impact
* destructive: false — mutation happens in the cloud, not locally
* requiresConsent: true
* supportsDryRun: true — dry-run shows the exact outbound payload
* affectedScope: ["network"]
* auditRequired: true
*/
import * as os from "os" ;
import { z } from "zod" ;
import { httpPost } from "./_shared/platform" ;
import type { Idp } from "./_shared/idp" ;
/**
* Local copy of getAgentId() — mirrors electron/agent/agentId.ts.
* Duplicated here because the skill compiles with rootDir=mcp/skills so
* it cannot import from the electron/ tree. Behaviour must stay in
* lock-step: trimmed AGENT_ID env var, empty string when unset.
*/
function getAgentId () : string {
const raw = process.env[ "AGENT_ID" ];
if (raw === undefined ) return "" ;
const trimmed = raw. trim ();
return trimmed;
}
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "request_idemeum_idp_reset" ,
description:
"Fallback path when the IDP's self-service portal is unavailable. POSTs " +
"to idemeum cloud, which holds admin-delegated IDP credentials per " +
"customer and triggers or performs the reset via the IDP admin API. " +
"The agent never sees IDP admin credentials. Response indicates how the " +
"reset was delivered (e.g. temp password emailed to recovery address)." ,
riskLevel: "high" ,
destructive: false ,
requiresConsent: true ,
supportsDryRun: true ,
affectedScope: [ "network" ],
auditRequired: true ,
schema: {
idp: z
. enum ([ "okta" , "entra" , "google" ])
. describe ( "IDP identifier; JumpCloud/Ping deferred to Wave 2." ),
username: z
. string ()
. min ( 1 )
. describe ( "The user's IDP login (email / UPN)." ),
tenant: z
. string ()
. optional ()
. describe ( "IDP tenant slug (Okta) or directory id (Entra); optional for Google." ),
dryRun: z
. boolean ()
. optional ()
. describe (
"When true, return { willPost, endpoint, payloadWithoutSecrets } " +
"without posting. The Bearer token is never included even in dry-run." ,
),
},
} as const ;
// -- Types --------------------------------------------------------------------
/** Wire-format body POSTed to idemeum cloud. */
interface CloudResetPayload {
agentId : string ;
username : string ;
idp : Idp ;
tenant : string | null ;
platform : "darwin" | "win32" | "other" ;
}
export type CloudResetStatus =
| "initiated"
| "failed"
| "not-eligible"
| "not-configured" ;
export interface CloudResetResult {
status : CloudResetStatus ;
message : string ;
deliveryMethod ?: "email" | "sms" | "helpdesk-ticket" ;
ticketId ?: string ;
/** Present on dry-run. */
willPost ?: boolean ;
endpoint ?: string ;
payloadWithoutSecrets ?: CloudResetPayload ;
/** Present on network / http errors. */
httpStatus ?: number ;
failureReason ?: "timeout" | "network" | "http" | "parse" ;
}
// -- Implementation -----------------------------------------------------------
function resolvePlatform () : "darwin" | "win32" | "other" {
const p = os. platform ();
if (p === "darwin" ) return "darwin" ;
if (p === "win32" ) return "win32" ;
return "other" ;
}
function buildPayload ( args : {
idp : Idp ; username : string ; tenant ?: string ;
}) : CloudResetPayload {
return {
agentId: getAgentId (),
username: args.username,
idp: args.idp,
tenant: args.tenant && args.tenant. length > 0 ? args.tenant : null ,
platform: resolvePlatform (),
};
}
/** Exported so tests can drive the payload builder directly. */
export const __testing = { buildPayload };
// -- Exported run function ----------------------------------------------------
export async function run ( args : {
idp : Idp ;
username : string ;
tenant ?: string ;
dryRun ?: boolean ;
}) : Promise < CloudResetResult > {
const url = process.env[ "IDEMEUM_IDP_URL" ];
const apiKey = process.env[ "IDEMEUM_IDP_API_KEY" ];
if ( ! url || url. length === 0 ) {
return {
status: "not-configured" ,
message:
"idemeum cloud fallback is not configured on this machine. " +
"Contact your MSP administrator to enable IDEMEUM_IDP_URL." ,
};
}
const endpoint = url. replace ( / \/ $ / , "" ) + "/v1/password-reset" ;
const payload = buildPayload (args);
if (args.dryRun) {
return {
status: "initiated" , // semantic placeholder — dry-run never executes
message: `Would POST idemeum cloud reset request for ${ args . username } (${ args . idp }).` ,
willPost: true ,
endpoint,
// NEVER include the Bearer token in dry-run output.
payloadWithoutSecrets: payload,
};
}
const headers : Record < string , string > = {
"content-type" : "application/json" ,
"accept" : "application/json" ,
};
if (apiKey && apiKey. length > 0 ) {
headers[ "Authorization" ] = `Bearer ${ apiKey }` ;
}
const body = JSON . stringify (payload);
const r = await httpPost (endpoint, body, headers, { timeoutMs: 10_000 });
if (r.failureReason) {
return {
status: "failed" ,
failureReason: r.failureReason,
message:
r.failureReason === "timeout"
? `idemeum cloud reset request timed out contacting ${ endpoint }.`
: `Could not reach idemeum cloud at ${ endpoint }.` ,
};
}
if (r.statusCode < 200 || r.statusCode >= 300 ) {
return {
status: "failed" ,
failureReason: "http" ,
httpStatus: r.statusCode,
message:
`idemeum cloud returned HTTP ${ r . statusCode }. ` +
`Please contact your MSP administrator.` ,
};
}
let parsed : Record < string , unknown >;
try {
parsed = JSON . parse (r.body) as Record < string , unknown >;
} catch {
return {
status: "failed" ,
failureReason: "parse" ,
httpStatus: r.statusCode,
message: "idemeum cloud returned a non-JSON response." ,
};
}
const rawStatus = typeof parsed[ "status" ] === "string" ? (parsed[ "status" ] as string ) : "" ;
const rawMessage = typeof parsed[ "message" ] === "string" ? (parsed[ "message" ] as string ) : "" ;
const rawDelivery = typeof parsed[ "deliveryMethod" ] === "string"
? (parsed[ "deliveryMethod" ] as string )
: undefined ;
const rawTicketId = typeof parsed[ "ticketId" ] === "string"
? (parsed[ "ticketId" ] as string )
: undefined ;
const status : CloudResetStatus =
rawStatus === "initiated" || rawStatus === "failed" || rawStatus === "not-eligible"
? rawStatus
: "failed" ;
const deliveryMethod =
rawDelivery === "email" || rawDelivery === "sms" || rawDelivery === "helpdesk-ticket"
? rawDelivery
: undefined ;
return {
status,
message: rawMessage. length > 0
? rawMessage
: `idemeum cloud responded with status "${ status }".` ,
deliveryMethod,
ticketId: rawTicketId,
};
}
// -- CLI smoke test -----------------------------------------------------------
if ( false ) {
run ({ idp: "okta" , username: "alice@example.com" , tenant: "acme" , dryRun: true })
. then (( r ) => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}