Files
openclaw/src/shared/device-bootstrap-profile.ts
Agustin Rivera b8372a714c fix(auth): bound bootstrap handoff scopes (#72919)
* fix(auth): bound bootstrap handoff scopes

Co-authored-by: zsx <git@zsxsoft.com>

* fix(auth): log stripped bootstrap scopes

* docs: add changelog entry for bootstrap handoff scope bounds

---------

Co-authored-by: zsx <git@zsxsoft.com>
Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-29 14:11:16 -06:00

81 lines
2.3 KiB
TypeScript

import { normalizeDeviceAuthRole, normalizeDeviceAuthScopes } from "./device-auth.js";
export type DeviceBootstrapProfile = {
roles: string[];
scopes: string[];
};
export type DeviceBootstrapProfileInput = {
roles?: readonly string[];
scopes?: readonly string[];
};
export const BOOTSTRAP_HANDOFF_OPERATOR_SCOPES = [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
] as const;
const BOOTSTRAP_HANDOFF_OPERATOR_SCOPE_SET = new Set<string>(BOOTSTRAP_HANDOFF_OPERATOR_SCOPES);
export const PAIRING_SETUP_BOOTSTRAP_PROFILE: DeviceBootstrapProfile = {
roles: ["node", "operator"],
scopes: [...BOOTSTRAP_HANDOFF_OPERATOR_SCOPES],
};
export function resolveBootstrapProfileScopesForRole(
role: string,
scopes: readonly string[],
): string[] {
const normalizedRole = normalizeDeviceAuthRole(role);
const normalizedScopes = normalizeDeviceAuthScopes(Array.from(scopes));
if (normalizedRole === "operator") {
return normalizedScopes.filter((scope) => BOOTSTRAP_HANDOFF_OPERATOR_SCOPE_SET.has(scope));
}
return [];
}
export function resolveBootstrapProfileScopesForRoles(
roles: readonly string[],
scopes: readonly string[],
): string[] {
return normalizeDeviceAuthScopes(
roles.flatMap((role) => resolveBootstrapProfileScopesForRole(role, scopes)),
);
}
export function normalizeDeviceBootstrapHandoffProfile(
input: DeviceBootstrapProfileInput | undefined,
): DeviceBootstrapProfile {
const profile = normalizeDeviceBootstrapProfile(input);
// Bootstrap handoff profiles can only carry the documented handoff allowlist.
return {
roles: profile.roles,
scopes: resolveBootstrapProfileScopesForRoles(profile.roles, profile.scopes),
};
}
function normalizeBootstrapRoles(roles: readonly string[] | undefined): string[] {
if (!Array.isArray(roles)) {
return [];
}
const out = new Set<string>();
for (const role of roles) {
const normalized = normalizeDeviceAuthRole(role);
if (normalized) {
out.add(normalized);
}
}
return [...out].toSorted();
}
export function normalizeDeviceBootstrapProfile(
input: DeviceBootstrapProfileInput | undefined,
): DeviceBootstrapProfile {
return {
roles: normalizeBootstrapRoles(input?.roles),
scopes: normalizeDeviceAuthScopes(input?.scopes ? [...input.scopes] : []),
};
}