import { getRuntimeConfig } from "../config/io.js"; import { hasEffectivePairedDeviceRole, listDevicePairing, type DeviceAuthToken, type PairedDevice, } from "../infra/device-pairing.js"; import { formatErrorMessage } from "../infra/errors.js"; import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js"; import { clearApnsRegistrationIfCurrent, loadApnsRegistration, resolveApnsAuthConfigFromEnv, resolveApnsRelayConfigFromEnv, sendApnsExecApprovalAlert, sendApnsExecApprovalResolvedWake, shouldClearStoredApnsRegistration, type ApnsAuthConfig, type ApnsRegistration, type ApnsRelayConfig, } from "../infra/push-apns.js"; import { roleScopesAllow } from "../shared/operator-scope-compat.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; const APPROVALS_SCOPE = "operator.approvals"; const OPERATOR_ROLE = "operator"; type GatewayLikeLogger = { debug?: (message: string) => void; warn?: (message: string) => void; error?: (message: string) => void; }; type ApprovalPushTarget = { deviceId: string; scopes: readonly string[]; }; type DeliveryTarget = { nodeId: string; registration: ApnsRegistration; }; type DeliveryPlan = { targets: DeliveryTarget[]; directAuth?: ApnsAuthConfig; relayConfig?: ApnsRelayConfig; }; type ApprovalDeliveryState = { nodeIds: string[]; requestPushPromise: Promise<{ attempted: number; delivered: number }>; }; function isIosPlatform(platform: string | undefined): boolean { const normalized = normalizeOptionalLowercaseString(platform) ?? ""; return normalized.startsWith("ios") || normalized.startsWith("ipados"); } function resolveActiveOperatorToken(device: PairedDevice): DeviceAuthToken | null { const operatorToken = device.tokens?.[OPERATOR_ROLE]; if (!operatorToken || operatorToken.revokedAtMs) { return null; } return operatorToken; } function canApproveExecRequests(device: PairedDevice): boolean { const operatorToken = resolveActiveOperatorToken(device); if (!operatorToken) { return false; } return roleScopesAllow({ role: OPERATOR_ROLE, requestedScopes: [APPROVALS_SCOPE], allowedScopes: operatorToken.scopes, }); } function shouldTargetDevice(params: { device: PairedDevice; requireApprovalScope: boolean; }): boolean { if (!isIosPlatform(params.device.platform)) { return false; } if (!hasEffectivePairedDeviceRole(params.device, OPERATOR_ROLE)) { return false; } if (!params.requireApprovalScope) { return true; } return canApproveExecRequests(params.device); } async function loadRegisteredTargets(params: { deviceIds: readonly string[]; }): Promise { const targets = await Promise.all( params.deviceIds.map(async (nodeId) => { const registration = await loadApnsRegistration(nodeId); return registration ? { nodeId, registration } : null; }), ); return targets.filter((target): target is DeliveryTarget => target !== null); } async function resolvePairedTargets(params: { requireApprovalScope: boolean; isTargetVisible?: (target: ApprovalPushTarget) => boolean; }): Promise { const pairing = await listDevicePairing(); const deviceIds = pairing.paired .filter((device) => { if (!shouldTargetDevice({ device, requireApprovalScope: params.requireApprovalScope })) { return false; } const operatorToken = resolveActiveOperatorToken(device); if ( params.isTargetVisible && !params.isTargetVisible({ deviceId: device.deviceId, scopes: operatorToken?.scopes ?? [], }) ) { return false; } return true; }) .map((device) => device.deviceId); return await loadRegisteredTargets({ deviceIds }); } async function resolveDeliveryPlan(params: { requireApprovalScope: boolean; explicitNodeIds?: readonly string[]; isTargetVisible?: (target: ApprovalPushTarget) => boolean; log: GatewayLikeLogger; }): Promise { const targets = params.explicitNodeIds?.length ? await loadRegisteredTargets({ deviceIds: params.explicitNodeIds }) : await resolvePairedTargets({ requireApprovalScope: params.requireApprovalScope, isTargetVisible: params.isTargetVisible, }); if (targets.length === 0) { return { targets: [] }; } const needsDirect = targets.some((target) => target.registration.transport === "direct"); const needsRelay = targets.some((target) => target.registration.transport === "relay"); let directAuth: ApnsAuthConfig | undefined; if (needsDirect) { const auth = await resolveApnsAuthConfigFromEnv(process.env); if (auth.ok) { directAuth = auth.value; } else { params.log.warn?.(`exec approvals: iOS direct APNs auth unavailable: ${auth.error}`); } } let relayConfig: ApnsRelayConfig | undefined; if (needsRelay) { const relay = resolveApnsRelayConfigFromEnv(process.env, getRuntimeConfig().gateway); if (relay.ok) { relayConfig = relay.value; } else { params.log.warn?.(`exec approvals: iOS relay APNs config unavailable: ${relay.error}`); } } return { targets: targets.filter((target) => target.registration.transport === "direct" ? Boolean(directAuth) : Boolean(relayConfig), ), directAuth, relayConfig, }; } async function clearStaleApnsRegistrationIfNeeded(params: { nodeId: string; registration: ApnsRegistration; result: { status: number; reason?: string }; }): Promise { if ( shouldClearStoredApnsRegistration({ registration: params.registration, result: params.result, }) ) { await clearApnsRegistrationIfCurrent({ nodeId: params.nodeId, registration: params.registration, }); } } async function sendRequestedPushes(params: { request: ExecApprovalRequest; plan: DeliveryPlan; log: GatewayLikeLogger; }): Promise<{ attempted: number; delivered: number }> { const results = await Promise.allSettled( params.plan.targets.map(async (target) => { const result = target.registration.transport === "direct" ? await sendApnsExecApprovalAlert({ registration: target.registration, nodeId: target.nodeId, approvalId: params.request.id, auth: params.plan.directAuth!, }) : await sendApnsExecApprovalAlert({ registration: target.registration, nodeId: target.nodeId, approvalId: params.request.id, relayConfig: params.plan.relayConfig!, }); await clearStaleApnsRegistrationIfNeeded({ nodeId: target.nodeId, registration: target.registration, result, }); if (!result.ok) { params.log.warn?.( `exec approvals: iOS request push failed node=${target.nodeId} status=${result.status} reason=${result.reason ?? "unknown"}`, ); } return { nodeId: target.nodeId, ok: result.ok }; }), ); for (const result of results) { if (result.status === "rejected") { const message = formatErrorMessage(result.reason); params.log.warn?.(`exec approvals: iOS request push threw error: ${message}`); } } return { attempted: params.plan.targets.length, delivered: results.filter((result) => result.status === "fulfilled" && result.value.ok).length, }; } async function sendResolvedPushes(params: { approvalId: string; plan: DeliveryPlan; log: GatewayLikeLogger; }): Promise { await Promise.allSettled( params.plan.targets.map(async (target) => { const result = target.registration.transport === "direct" ? await sendApnsExecApprovalResolvedWake({ registration: target.registration, nodeId: target.nodeId, approvalId: params.approvalId, auth: params.plan.directAuth!, }) : await sendApnsExecApprovalResolvedWake({ registration: target.registration, nodeId: target.nodeId, approvalId: params.approvalId, relayConfig: params.plan.relayConfig!, }); await clearStaleApnsRegistrationIfNeeded({ nodeId: target.nodeId, registration: target.registration, result, }); if (!result.ok) { params.log.warn?.( `exec approvals: iOS cleanup push failed node=${target.nodeId} status=${result.status} reason=${result.reason ?? "unknown"}`, ); } }), ); } export function createExecApprovalIosPushDelivery(params: { log: GatewayLikeLogger }) { const approvalDeliveriesById = new Map(); const pendingDeliveryStateById = new Map>(); return { async handleRequested( request: ExecApprovalRequest, opts?: { isTargetVisible?: (target: ApprovalPushTarget) => boolean }, ): Promise { const deliveryStatePromise = (async (): Promise => { const plan = await resolveDeliveryPlan({ requireApprovalScope: true, isTargetVisible: opts?.isTargetVisible, log: params.log, }); if (plan.targets.length === 0) { approvalDeliveriesById.delete(request.id); return null; } const deliveryState: ApprovalDeliveryState = { nodeIds: plan.targets.map((target) => target.nodeId), requestPushPromise: sendRequestedPushes({ request, plan, log: params.log }).catch( (err) => { const message = formatErrorMessage(err); params.log.error?.(`exec approvals: iOS request push failed: ${message}`); return { attempted: plan.targets.length, delivered: 0 }; }, ), }; approvalDeliveriesById.set(request.id, deliveryState); return deliveryState; })(); pendingDeliveryStateById.set(request.id, deliveryStatePromise); const deliveryState = await deliveryStatePromise; if (pendingDeliveryStateById.get(request.id) === deliveryStatePromise) { pendingDeliveryStateById.delete(request.id); } if (!deliveryState) { return false; } const { attempted, delivered } = await deliveryState.requestPushPromise; if (attempted > 0 && delivered === 0) { params.log.warn?.( `exec approvals: iOS request push reached no devices approvalId=${request.id} attempted=${attempted}`, ); if ( approvalDeliveriesById.get(request.id)?.requestPushPromise === deliveryState.requestPushPromise ) { approvalDeliveriesById.delete(request.id); } return false; } return true; }, async handleResolved(resolved: ExecApprovalResolved): Promise { const deliveryState = approvalDeliveriesById.get(resolved.id) ?? (await pendingDeliveryStateById.get(resolved.id)); approvalDeliveriesById.delete(resolved.id); pendingDeliveryStateById.delete(resolved.id); if (!deliveryState?.nodeIds.length) { params.log.debug?.( `exec approvals: iOS cleanup push skipped approvalId=${resolved.id} reason=missing-targets`, ); return; } await deliveryState.requestPushPromise; const plan = await resolveDeliveryPlan({ requireApprovalScope: false, explicitNodeIds: deliveryState.nodeIds, log: params.log, }); if (plan.targets.length === 0) { return; } await sendResolvedPushes({ approvalId: resolved.id, plan, log: params.log, }); }, async handleExpired(request: ExecApprovalRequest): Promise { const deliveryState = approvalDeliveriesById.get(request.id) ?? (await pendingDeliveryStateById.get(request.id)); approvalDeliveriesById.delete(request.id); pendingDeliveryStateById.delete(request.id); if (!deliveryState?.nodeIds.length) { params.log.debug?.( `exec approvals: iOS cleanup push skipped approvalId=${request.id} reason=missing-targets`, ); return; } await deliveryState.requestPushPromise; const plan = await resolveDeliveryPlan({ requireApprovalScope: false, explicitNodeIds: deliveryState.nodeIds, log: params.log, }); if (plan.targets.length === 0) { return; } await sendResolvedPushes({ approvalId: request.id, plan, log: params.log, }); }, }; }