openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
318 lines (317 loc) • 13.6 kB
JavaScript
import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js";
import { G as validateDevicePairListParams, J as validateDeviceTokenRevokeParams, K as validateDevicePairRejectParams, W as validateDevicePairApproveParams, Y as validateDeviceTokenRotateParams, q as validateDevicePairRemoveParams, t as formatValidationErrors } from "./src-oj0IwW6K.js";
import { a as getPairedDevice, d as rejectDevicePairing, f as removePairedDevice, g as summarizeDeviceTokens, h as rotateDeviceToken, i as formatDevicePairingForbiddenMessage, l as listDevicePairing, m as revokeDeviceToken, n as approveDevicePairing, o as getPendingDevicePairing } from "./device-pairing-Bcmp5CuV.js";
//#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) {
const { tokens, approvedScopes: _approvedScopes, ...rest } = device;
return {
...rest,
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 resolveDeviceManagementAuthz(client, targetDeviceId) {
return {
...resolveDeviceSessionAuthz(client),
normalizedTargetDeviceId: targetDeviceId.trim()
};
}
function resolveDeviceSessionAuthz(client) {
const callerScopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : [];
const rawCallerDeviceId = client?.connect?.device?.id;
return {
callerDeviceId: client?.isDeviceTokenAuth && typeof rawCallerDeviceId === "string" && rawCallerDeviceId.trim() ? rawCallerDeviceId.trim() : null,
callerScopes,
isAdminCaller: callerScopes.includes("operator.admin")
};
}
function deniesCrossDeviceManagement(authz) {
return Boolean(authz.callerDeviceId && authz.callerDeviceId !== authz.normalizedTargetDeviceId && !authz.isAdminCaller);
}
function shouldReturnRotatedDeviceToken(authz) {
return Boolean(authz.callerDeviceId && authz.callerDeviceId === authz.normalizedTargetDeviceId);
}
function deniesDeviceTokenRoleManagement(authz, targetRole) {
const normalizedTargetRole = targetRole.trim();
if (!normalizedTargetRole || authz.isAdminCaller) return false;
return normalizedTargetRole !== "operator";
}
function hasNonOperatorDeviceRole(input) {
const roles = /* @__PURE__ */ new Set();
const role = input.role?.trim();
if (role) roles.add(role);
for (const entry of input.roles ?? []) {
const normalized = entry.trim();
if (normalized) roles.add(normalized);
}
return [...roles].some((entry) => entry !== "operator");
}
function hasNonOperatorDeviceTokenRole(tokens) {
for (const token of Object.values(tokens ?? {})) {
const normalized = token.role.trim();
if (normalized && normalized !== "operator") return true;
}
return false;
}
function requestsNonOperatorDeviceRole(pending) {
return hasNonOperatorDeviceRole(pending);
}
function pairedDeviceHasNonOperatorRole(device) {
return hasNonOperatorDeviceRole(device) || hasNonOperatorDeviceTokenRole(device.tokens);
}
/** Gateway request handlers for device pair approval, removal, token rotation, and revocation. */
const deviceHandlers = {
"device.pair.list": async ({ params, respond, client }) => {
if (!validateDevicePairListParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid device.pair.list params: ${formatValidationErrors(validateDevicePairListParams.errors)}`));
return;
}
const list = await listDevicePairing();
const authz = resolveDeviceSessionAuthz(client);
const visibleList = authz.callerDeviceId && !authz.isAdminCaller ? {
pending: list.pending.filter((request) => request.deviceId.trim() === authz.callerDeviceId),
paired: list.paired.filter((device) => device.deviceId.trim() === authz.callerDeviceId)
} : list;
respond(true, {
pending: visibleList.pending,
paired: visibleList.paired.map((device) => redactPairedDevice(device))
}, void 0);
},
"device.pair.approve": async ({ params, respond, context, client }) => {
if (!validateDevicePairApproveParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid device.pair.approve params: ${formatValidationErrors(validateDevicePairApproveParams.errors)}`));
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`);
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`);
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") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, formatDevicePairingForbiddenMessage(approved)));
return;
}
context.logGateway.info(`device pairing approved device=${approved.device.deviceId} role=${approved.device.role ?? "unknown"}`);
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);
},
"device.pair.reject": async ({ params, respond, context, client }) => {
if (!validateDevicePairRejectParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid device.pair.reject params: ${formatValidationErrors(validateDevicePairRejectParams.errors)}`));
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`);
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.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 (!validateDevicePairRemoveParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid device.pair.remove params: ${formatValidationErrors(validateDevicePairRemoveParams.errors)}`));
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`);
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`);
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;
}
context.logGateway.info(`device pairing removed device=${removed.deviceId}`);
context.invalidateClientsForDevice?.(removed.deviceId, { reason: "device-pair-removed" });
respond(true, removed, void 0);
queueMicrotask(() => {
context.disconnectClientsForDevice?.(removed.deviceId);
});
},
"device.token.rotate": async ({ params, respond, context, client }) => {
if (!validateDeviceTokenRotateParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid device.token.rotate params: ${formatValidationErrors(validateDeviceTokenRotateParams.errors)}`));
return;
}
const { deviceId, role, scopes } = params;
const authz = resolveDeviceManagementAuthz(client, deviceId);
if (deniesCrossDeviceManagement(authz)) {
logDeviceTokenRotationDenied({
log: context.logGateway,
deviceId,
role,
reason: "device-ownership-mismatch"
});
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"
});
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_ROTATION_DENIED_MESSAGE));
return;
}
const rotated = await rotateDeviceToken({
deviceId,
role,
scopes,
callerScopes: authz.callerScopes
});
if (!rotated.ok) {
logDeviceTokenRotationDenied({
log: context.logGateway,
deviceId,
role,
reason: rotated.reason,
scope: rotated.scope
});
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_ROTATION_DENIED_MESSAGE));
return;
}
const entry = rotated.entry;
context.logGateway.info(`device token rotated device=${deviceId} role=${entry.role} scopes=${entry.scopes.join(",")}`);
context.invalidateClientsForDevice?.(deviceId.trim(), {
role: entry.role,
reason: "device-token-rotated"
});
respond(true, {
deviceId,
role: entry.role,
...shouldReturnRotatedDeviceToken(authz) ? { token: entry.token } : {},
scopes: entry.scopes,
rotatedAtMs: entry.rotatedAtMs ?? entry.createdAtMs
}, void 0);
queueMicrotask(() => {
context.disconnectClientsForDevice?.(deviceId.trim(), { role: entry.role });
});
},
"device.token.revoke": async ({ params, respond, context, client }) => {
if (!validateDeviceTokenRevokeParams(params)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid device.token.revoke params: ${formatValidationErrors(validateDeviceTokenRevokeParams.errors)}`));
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`);
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"
});
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, DEVICE_TOKEN_REVOCATION_DENIED_MESSAGE));
return;
}
const revoked = await revokeDeviceToken({
deviceId,
role,
callerScopes: authz.callerScopes
});
if (!revoked.ok) {
logDeviceTokenRevocationDenied({
log: context.logGateway,
deviceId,
role,
reason: revoked.reason,
scope: revoked.scope
});
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}`);
context.invalidateClientsForDevice?.(normalizedDeviceId, {
role: entry.role,
reason: "device-token-revoked"
});
respond(true, {
deviceId: normalizedDeviceId,
role: entry.role,
revokedAtMs: entry.revokedAtMs ?? Date.now()
}, void 0);
queueMicrotask(() => {
context.disconnectClientsForDevice?.(normalizedDeviceId, { role: entry.role });
});
}
};
//#endregion
export { deviceHandlers };