UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

674 lines (673 loc) 26.5 kB
import { l as isOperatorScope } from "./operator-scopes-Dw7Gu2cA.js"; import { t as ConnectErrorDetailCodes } from "./connect-error-details-DaOfGNN8.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { n as normalizeDeviceAuthScopes } from "./device-auth-C-STNejO.js"; import { r as roleScopesAllow } from "./operator-scope-compat-iV7_Lmth.js"; import { At as validateDeviceTokenRevokeParams, Ct as validateDevicePairApproveParams, Dt as validateDevicePairRenameParams, Et as validateDevicePairRemoveParams, Tt as validateDevicePairRejectParams, dr as validateScopeUpgradeWait, jt as validateDeviceTokenRotateParams, ur as validateScopeUpgradeRequest, wt as validateDevicePairListParams } from "./src-BiL5aQto.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { s as resolveOperatorRolePolicy } from "./operator-role-policy-wsr1DeJv.js"; import { c as listDevicePairing, f as rejectDevicePairing, h as requestDevicePairing, n as getPairedDevice, p as removePairedDevice, r as getPendingDevicePairing, v as updatePairedDeviceMetadata } from "./device-pairing-DFIQp3ZY.js"; import { t as holdGatewayPolicyResponse } from "./ws-policy-close-CZEENNh9.js"; import { a as rotateDeviceToken, i as revokeDeviceToken, o as summarizeDeviceTokens } from "./device-pairing-tokens-GP7d0q7h.js"; import { n as approveDevicePairing, r as formatDevicePairingForbiddenMessage } from "./device-pairing-approval-LumnRImV.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { t as GATEWAY_EVENT_DEVICE_PAIR_CHANGED } from "./events-DFzVQFw-.js"; import { n as clearRemovedNodeRuntimeState, s as reconcileRevokedDeviceWorker } from "./node-runtime-state-Cp7qgqDe.js"; import { a as invalidateNodeWakeState } from "./node-wake-state-CWVR-GCk.js"; import { a as requestsNonOperatorDeviceRole, i as pairedDeviceHasNonOperatorRole, n as deniesCrossDeviceManagement, o as resolveDeviceManagementAuthz, r as deniesDeviceTokenRoleManagement, s as resolveDeviceSessionAuthz, t as emitDeviceManagementSecurityEvent } from "./device-management-security-BqfIxdra.js"; //#region src/gateway/server-methods/device-scope-upgrade.ts const DEVICE_REQUIRED_MESSAGE = "device scope upgrade requires a paired browser identity; reopen the Control UI over HTTPS or localhost, then retry"; function readUpgradeOwner(client) { const deviceId = client?.connect.device?.id.trim(); const publicKey = client?.connect.device?.publicKey.trim(); return client?.connId && client.connect.role === "operator" && deviceId && publicKey ? { deviceId, publicKey } : null; } function respondDeviceRequired(respond) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_REQUIRED_MESSAGE, { details: { code: ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED, recommendedNextStep: "reopen_control_ui_securely" } })); } /** Live operator scope-upgrade request and identity-bound wait handlers. */ const scopeUpgradeHandlers = { "device.scopes.requestUpgrade": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateScopeUpgradeRequest, "device.scopes.requestUpgrade", respond)) return; const owner = readUpgradeOwner(client); if (!owner) { respondDeviceRequired(respond); return; } const paired = await getPairedDevice(owner.deviceId); if (!paired || paired.publicKey !== owner.publicKey) { respondDeviceRequired(respond); return; } const requestedScopes = normalizeDeviceAuthScopes(params.scopes); if (!requestedScopes.every(isOperatorScope)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "requested scopes contain an unknown operator scope")); return; } const rolePolicy = resolveOperatorRolePolicy(client, context.getRuntimeConfig()); if (rolePolicy && !roleScopesAllow({ role: "operator", requestedScopes, allowedScopes: rolePolicy.scopes })) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "requested scopes exceed your assigned operator role; ask a gateway administrator to change your role")); return; } const currentScopes = Array.isArray(client?.connect.scopes) ? client.connect.scopes : []; if (!roleScopesAllow({ role: "operator", requestedScopes: currentScopes, allowedScopes: requestedScopes })) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "requested scopes must include the connection's current scopes")); return; } const pairing = await requestDevicePairing({ deviceId: owner.deviceId, publicKey: owner.publicKey, displayName: client?.connect.client.displayName, platform: client?.connect.client.platform, deviceFamily: client?.connect.client.deviceFamily, clientId: client?.connect.client.id, clientMode: client?.connect.client.mode, browserOrigin: paired.browserOrigin, role: "operator", scopes: requestedScopes, remoteIp: client?.clientIp, silent: false }); const coordinator = context.scopeUpgradeCoordinator; if (!coordinator?.register({ requestId: pairing.request.requestId, expiresAtMs: pairing.expiresAtMs, owner, requestedScopes, initialToken: paired.tokens?.operator?.token, initialApprovedAtMs: paired.approvedAtMs })) { respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "device scope upgrade is temporarily unavailable", { retryable: true })); return; } const resolvedAt = Date.now(); for (const superseded of pairing.superseded ?? []) { coordinator.notify(superseded.requestId, "rejected"); context.broadcast("device.pair.resolved", { requestId: superseded.requestId, deviceId: superseded.deviceId, decision: "rejected", ts: resolvedAt }, { dropIfSlow: true }); } if (pairing.created) context.broadcast("device.pair.requested", pairing.request, { dropIfSlow: true }); context.logGateway.warn(`security audit: live device scope upgrade requested device=${owner.deviceId} scopesFrom=${currentScopes.join(",")} scopesTo=${requestedScopes.join(",")}`); respond(true, { requestId: pairing.request.requestId }, void 0); }, "device.scopes.waitUpgrade": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateScopeUpgradeWait, "device.scopes.waitUpgrade", respond)) return; const owner = readUpgradeOwner(client); if (!owner) { respondDeviceRequired(respond); return; } const requestId = params.requestId; const result = await context.scopeUpgradeCoordinator?.wait(requestId, owner); if (!result) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "scope upgrade expired or not found")); return; } if (result.status === "approved") { const rolePolicy = resolveOperatorRolePolicy(client, context.getRuntimeConfig()); if (rolePolicy && !roleScopesAllow({ role: "operator", requestedScopes: result.scopes, allowedScopes: rolePolicy.scopes })) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "approved scopes exceed your assigned operator role; ask a gateway administrator to change your role")); return; } } respond(true, result, void 0); } }; //#endregion //#region src/gateway/server-methods/devices.ts const DEVICE_TOKEN_ROTATION_DENIED_MESSAGE = "device token rotation denied"; const DEVICE_TOKEN_REVOCATION_DENIED_MESSAGE = "device token revocation denied"; const DEVICE_PAIR_APPROVAL_DENIED_MESSAGE = "device pairing approval denied"; const DEVICE_PAIR_REJECTION_DENIED_MESSAGE = "device pairing rejection denied"; function redactPairedDevice(device, opts) { const { tokens, approvedScopes: _approvedScopes, ...rest } = device; return { ...rest, ...opts?.connected !== void 0 ? { connected: opts.connected } : {}, tokens: summarizeDeviceTokens(tokens) }; } function logDeviceTokenRotationDenied(params) { const suffix = params.scope ? ` scope=${params.scope}` : ""; params.log.warn(`device token rotation denied device=${params.deviceId} role=${params.role} reason=${params.reason}${suffix}`); } function logDeviceTokenRevocationDenied(params) { const suffix = params.scope ? ` scope=${params.scope}` : ""; params.log.warn(`device token revocation denied device=${params.deviceId} role=${params.role} reason=${params.reason}${suffix}`); } function shouldReturnRotatedDeviceToken(authz) { return Boolean(authz.callerDeviceId && authz.callerDeviceId === authz.normalizedTargetDeviceId); } function emitDeviceSecurityEvent(params) { emitDeviceManagementSecurityEvent(params); } function emitDevicePairingDeniedSecurityEvent(params) { emitDeviceSecurityEvent({ action: "device.pairing.denied", outcome: "denied", severity: params.severity ?? "medium", authz: params.authz, targetDeviceId: params.targetDeviceId, policyId: "gateway.device-pairing", decision: "deny", controlId: params.controlId, reason: params.reason }); } function emitDevicePairingLifecycleSecurityEvent(params) { emitDeviceSecurityEvent({ action: params.action, outcome: "success", severity: params.severity, authz: params.authz, targetDeviceId: params.targetDeviceId, policyId: "gateway.device-pairing", decision: "allow", controlId: params.controlId, attributes: params.attributes }); } function emitDeviceTokenDeniedSecurityEvent(params) { emitDeviceSecurityEvent({ action: params.action, outcome: "denied", severity: "medium", authz: params.authz, targetDeviceId: params.targetDeviceId, policyId: "gateway.device-token", decision: "deny", controlId: params.controlId, reason: params.reason, attributes: { role: params.role.trim() } }); } function emitDeviceTokenLifecycleSecurityEvent(params) { emitDeviceSecurityEvent({ action: params.action, outcome: "success", severity: params.severity, authz: params.authz, targetDeviceId: params.targetDeviceId, policyId: "gateway.device-token", decision: "allow", controlId: params.controlId, attributes: { role: params.role, ...params.scopeCount !== void 0 ? { scope_count: params.scopeCount } : {} } }); } /** Gateway request handlers for device pair approval, removal, token rotation, and revocation. */ const deviceHandlers = { ...scopeUpgradeHandlers, "device.pair.list": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDevicePairListParams, "device.pair.list", respond)) return; const list = await listDevicePairing(); const authz = resolveDeviceSessionAuthz(client); let visibleList = list; if (authz.callerDeviceId && !authz.isAdminCaller) visibleList = { pending: list.pending.filter((request) => request.deviceId.trim() === authz.callerDeviceId), paired: list.paired.filter((device) => device.deviceId.trim() === authz.callerDeviceId) }; respond(true, { pending: visibleList.pending, paired: visibleList.paired.map((device) => redactPairedDevice(device, { connected: context.hasConnectedClientsForDevice?.(device.deviceId.trim()) ?? false })) }, void 0); }, "device.pair.approve": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDevicePairApproveParams, "device.pair.approve", respond)) return; const { requestId } = params; const authz = resolveDeviceSessionAuthz(client); if (!authz.isAdminCaller) { const pending = await getPendingDevicePairing(requestId); if (!pending) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_PAIR_APPROVAL_DENIED_MESSAGE)); return; } if (authz.callerDeviceId && pending.deviceId.trim() !== authz.callerDeviceId) { context.logGateway.warn(`device pairing approval denied request=${requestId} reason=device-ownership-mismatch`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: pending.deviceId, controlId: "device.pair.approve", reason: "device-ownership-mismatch" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_PAIR_APPROVAL_DENIED_MESSAGE)); return; } if (requestsNonOperatorDeviceRole(pending)) { context.logGateway.warn(`device pairing approval denied request=${requestId} reason=role-management-requires-admin`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: pending.deviceId, controlId: "device.pair.approve", reason: "role-management-requires-admin" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_PAIR_APPROVAL_DENIED_MESSAGE)); return; } } const approved = await approveDevicePairing(requestId, { callerScopes: authz.callerScopes }); if (!approved) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown requestId")); return; } if (approved.status === "forbidden") { emitDevicePairingDeniedSecurityEvent({ authz, controlId: "device.pair.approve", reason: approved.reason }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, formatDevicePairingForbiddenMessage(approved))); return; } const normalizedDeviceId = approved.device.deviceId.trim(); context.scopeUpgradeCoordinator?.notify(requestId, "approved"); if (approved.nodePairingGenerationChanged) { invalidateNodeWakeState(normalizedDeviceId); context.invalidateClientsForDevice?.(normalizedDeviceId, { role: "node", reason: "device-pairing-reapproved" }); } context.logGateway.info(`device pairing approved device=${approved.device.deviceId} role=${approved.device.role ?? "unknown"}`); emitDevicePairingLifecycleSecurityEvent({ action: "device.pairing.approved", severity: "low", authz, targetDeviceId: approved.device.deviceId, controlId: "device.pair.approve", attributes: { role_count: approved.device.roles?.length ?? (approved.device.role ? 1 : 0), scope_count: approved.device.approvedScopes?.length ?? approved.device.scopes?.length ?? 0 } }); context.broadcast("device.pair.resolved", { requestId, deviceId: approved.device.deviceId, decision: "approved", ts: Date.now() }, { dropIfSlow: true }); respond(true, { requestId, device: redactPairedDevice(approved.device) }, void 0); if (approved.nodePairingGenerationChanged) queueMicrotask(() => { context.disconnectClientsForDevice?.(normalizedDeviceId, { role: "node" }); }); }, "device.pair.reject": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDevicePairRejectParams, "device.pair.reject", respond)) return; const { requestId } = params; const authz = resolveDeviceSessionAuthz(client); if (authz.callerDeviceId && !authz.isAdminCaller) { const pending = await getPendingDevicePairing(requestId); if (!pending) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_PAIR_REJECTION_DENIED_MESSAGE)); return; } if (pending.deviceId.trim() !== authz.callerDeviceId) { context.logGateway.warn(`device pairing rejection denied request=${requestId} reason=device-ownership-mismatch`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: pending.deviceId, controlId: "device.pair.reject", reason: "device-ownership-mismatch" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_PAIR_REJECTION_DENIED_MESSAGE)); return; } } const rejected = await rejectDevicePairing(requestId); if (!rejected) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown requestId")); return; } context.scopeUpgradeCoordinator?.notify(requestId, "rejected"); emitDevicePairingLifecycleSecurityEvent({ action: "device.pairing.rejected", authz, targetDeviceId: rejected.deviceId, controlId: "device.pair.reject", severity: "low" }); context.broadcast("device.pair.resolved", { requestId, deviceId: rejected.deviceId, decision: "rejected", ts: Date.now() }, { dropIfSlow: true }); respond(true, rejected, void 0); }, "device.pair.remove": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDevicePairRemoveParams, "device.pair.remove", respond)) return; const { deviceId } = params; const authz = resolveDeviceManagementAuthz(client, deviceId); if (deniesCrossDeviceManagement(authz)) { context.logGateway.warn(`device pairing removal denied device=${deviceId} reason=device-ownership-mismatch`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: deviceId, controlId: "device.pair.remove", reason: "device-ownership-mismatch" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "device pairing removal denied")); return; } if (authz.callerDeviceId && !authz.isAdminCaller) { const paired = await getPairedDevice(authz.normalizedTargetDeviceId); if (paired && pairedDeviceHasNonOperatorRole(paired)) { context.logGateway.warn(`device pairing removal denied device=${deviceId} reason=role-management-requires-admin`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: deviceId, controlId: "device.pair.remove", reason: "role-management-requires-admin" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "device pairing removal denied")); return; } } const removed = await removePairedDevice(deviceId); if (!removed) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown deviceId")); return; } clearRemovedNodeRuntimeState({ nodeId: removed.deviceId, context }); try { try { holdGatewayPolicyResponse(respond); } finally { context.invalidateClientsForDevice?.(removed.deviceId, { reason: "device-pair-removed" }); await reconcileRevokedDeviceWorker(context, removed.deviceId); } context.logGateway.info(`device pairing removed device=${removed.deviceId}`); emitDevicePairingLifecycleSecurityEvent({ action: "device.pairing.removed", severity: "medium", authz, targetDeviceId: removed.deviceId, controlId: "device.pair.remove" }); respond(true, removed, void 0); } finally { queueMicrotask(() => { context.disconnectClientsForDevice?.(removed.deviceId); }); } }, "device.pair.rename": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDevicePairRenameParams, "device.pair.rename", respond)) return; const { deviceId, label } = params; const trimmed = label.trim(); if (!trimmed) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "label required")); return; } const authz = resolveDeviceManagementAuthz(client, deviceId); if (deniesCrossDeviceManagement(authz)) { context.logGateway.warn(`device pairing rename denied device=${deviceId} reason=device-ownership-mismatch`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: deviceId, controlId: "device.pair.rename", reason: "device-ownership-mismatch" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "device pairing rename denied")); return; } if (authz.callerDeviceId && !authz.isAdminCaller) { const paired = await getPairedDevice(authz.normalizedTargetDeviceId); if (paired && pairedDeviceHasNonOperatorRole(paired)) { context.logGateway.warn(`device pairing rename denied device=${deviceId} reason=role-management-requires-admin`); emitDevicePairingDeniedSecurityEvent({ authz, targetDeviceId: deviceId, controlId: "device.pair.rename", reason: "role-management-requires-admin" }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "device pairing rename denied")); return; } } if (!await updatePairedDeviceMetadata(deviceId, { operatorLabel: trimmed })) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown deviceId")); return; } context.logGateway.info(`device pairing renamed device=${deviceId} label=${trimmed}`); emitDevicePairingLifecycleSecurityEvent({ action: "device.pairing.renamed", severity: "low", authz, targetDeviceId: deviceId, controlId: "device.pair.rename" }); context.broadcast(GATEWAY_EVENT_DEVICE_PAIR_CHANGED, {}, { dropIfSlow: true }); respond(true, { deviceId, label: trimmed }, void 0); }, "device.token.rotate": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDeviceTokenRotateParams, "device.token.rotate", respond)) return; const { deviceId, role, scopes } = params; const authz = resolveDeviceManagementAuthz(client, deviceId); if (deniesCrossDeviceManagement(authz)) { logDeviceTokenRotationDenied({ log: context.logGateway, deviceId, role, reason: "device-ownership-mismatch" }); emitDeviceTokenDeniedSecurityEvent({ action: "device.token.rotation_denied", authz, targetDeviceId: deviceId, controlId: "device.token.rotate", reason: "device-ownership-mismatch", role }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_ROTATION_DENIED_MESSAGE)); return; } if (deniesDeviceTokenRoleManagement(authz, role)) { logDeviceTokenRotationDenied({ log: context.logGateway, deviceId, role, reason: "role-management-requires-admin" }); emitDeviceTokenDeniedSecurityEvent({ action: "device.token.rotation_denied", authz, targetDeviceId: deviceId, controlId: "device.token.rotate", reason: "role-management-requires-admin", role }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_ROTATION_DENIED_MESSAGE)); return; } const callerScopes = role.trim() === "operator" ? authz.callerScopes : void 0; const rotated = await rotateDeviceToken({ deviceId, role, scopes, callerScopes }); if (!rotated.ok) { logDeviceTokenRotationDenied({ log: context.logGateway, deviceId, role, reason: rotated.reason, scope: rotated.scope }); emitDeviceTokenDeniedSecurityEvent({ action: "device.token.rotation_denied", authz, targetDeviceId: deviceId, controlId: "device.token.rotate", reason: rotated.reason, role }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_ROTATION_DENIED_MESSAGE)); return; } const entry = rotated.entry; const normalizedDeviceId = deviceId.trim(); context.logGateway.info(`device token rotated device=${deviceId} role=${entry.role} scopes=${entry.scopes.join(",")}`); emitDeviceTokenLifecycleSecurityEvent({ action: "device.token.rotated", severity: "medium", authz, targetDeviceId: deviceId, controlId: "device.token.rotate", role: entry.role, scopeCount: entry.scopes.length }); if (entry.role === "node") invalidateNodeWakeState(normalizedDeviceId); try { holdGatewayPolicyResponse(respond); } finally { context.invalidateClientsForDevice?.(normalizedDeviceId, { role: entry.role, reason: "device-token-rotated" }); queueMicrotask(() => { context.disconnectClientsForDevice?.(normalizedDeviceId, { role: entry.role }); }); } const deliversTokenInBand = shouldReturnRotatedDeviceToken(authz); respond(true, { deviceId, role: entry.role, ...deliversTokenInBand ? { token: entry.token } : {}, scopes: entry.scopes, rotatedAtMs: entry.rotatedAtMs ?? entry.createdAtMs, tokenDelivery: deliversTokenInBand ? "in-band" : "withheld-cross-device" }, void 0); }, "device.token.revoke": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateDeviceTokenRevokeParams, "device.token.revoke", respond)) return; const { deviceId, role } = params; const authz = resolveDeviceManagementAuthz(client, deviceId); if (deniesCrossDeviceManagement(authz)) { context.logGateway.warn(`device token revocation denied device=${deviceId} role=${role} reason=device-ownership-mismatch`); emitDeviceTokenDeniedSecurityEvent({ action: "device.token.revocation_denied", authz, targetDeviceId: deviceId, controlId: "device.token.revoke", reason: "device-ownership-mismatch", role }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_REVOCATION_DENIED_MESSAGE)); return; } if (deniesDeviceTokenRoleManagement(authz, role)) { logDeviceTokenRevocationDenied({ log: context.logGateway, deviceId, role, reason: "role-management-requires-admin" }); emitDeviceTokenDeniedSecurityEvent({ action: "device.token.revocation_denied", authz, targetDeviceId: deviceId, controlId: "device.token.revoke", reason: "role-management-requires-admin", role }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_REVOCATION_DENIED_MESSAGE)); return; } const callerScopes = role.trim() === "operator" ? authz.callerScopes : void 0; const revoked = await revokeDeviceToken({ deviceId, role, callerScopes }); if (!revoked.ok) { logDeviceTokenRevocationDenied({ log: context.logGateway, deviceId, role, reason: revoked.reason, scope: revoked.scope }); emitDeviceTokenDeniedSecurityEvent({ action: "device.token.revocation_denied", authz, targetDeviceId: deviceId, controlId: "device.token.revoke", reason: revoked.reason, role }); respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_REVOCATION_DENIED_MESSAGE)); return; } const entry = revoked.entry; const normalizedDeviceId = deviceId.trim(); context.logGateway.info(`device token revoked device=${normalizedDeviceId} role=${entry.role}`); emitDeviceTokenLifecycleSecurityEvent({ action: "device.token.revoked", severity: "high", authz, targetDeviceId: normalizedDeviceId, controlId: "device.token.revoke", role: entry.role }); if (entry.role === "node") { clearRemovedNodeRuntimeState({ nodeId: normalizedDeviceId, context }); await reconcileRevokedDeviceWorker(context, normalizedDeviceId); } try { holdGatewayPolicyResponse(respond); } finally { context.invalidateClientsForDevice?.(normalizedDeviceId, { role: entry.role, reason: "device-token-revoked" }); queueMicrotask(() => { context.disconnectClientsForDevice?.(normalizedDeviceId, { role: entry.role }); }); } respond(true, { deviceId: normalizedDeviceId, role: entry.role, revokedAtMs: entry.revokedAtMs ?? Date.now() }, void 0); } }; //#endregion export { deviceHandlers };