openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
250 lines (249 loc) • 12.3 kB
JavaScript
import { n as normalizeDeviceAuthScopes } from "./device-auth-C-STNejO.js";
import { n as resolveScopeOutsideRequestedRoles, t as resolveMissingRequestedScope } from "./operator-scope-compat-iV7_Lmth.js";
import { U as resolveDeviceProfileRoleScopes, W as resolveDeviceProfileScopes, g as generatePairingToken, w as persistDevicePairingStoreState } from "./device-bootstrap-DRFMRCcr.js";
import { A as withDevicePairingLock, C as mergeDevicePairingRoles, D as preserveDeviceRoleScopes, O as resolveRequestedDeviceRoles, S as loadDevicePairingState, g as resolveNodePairingGeneration, k as sameDevicePairingStringSet, o as invalidatePairedCardRendererCache, t as clearNodePairingGenerationState, w as mergeDevicePairingScopes } from "./device-pairing-DFIQp3ZY.js";
import { r as resolveRoleTokenScopes, t as createDeviceAuthToken } from "./device-pairing-tokens-GP7d0q7h.js";
//#region src/infra/device-pairing-approval.ts
const OPERATOR_ROLE = "operator";
const OPERATOR_SCOPE_PREFIX = "operator.";
/** Format a device-pairing authorization failure for CLI/API callers. */
function formatDevicePairingForbiddenMessage(result) {
switch (result.reason) {
case "caller-scopes-required": return `missing scope: ${result.scope ?? "callerScopes-required"}`;
case "caller-missing-scope": return `missing scope: ${result.scope ?? "unknown"}`;
case "scope-outside-requested-roles": return `invalid scope for requested roles: ${result.scope ?? "unknown"}`;
case "approval-policy-changed": return "automatic pairing policy changed; retry pairing or request manual approval";
case "bootstrap-role-not-allowed": return `bootstrap profile does not allow role: ${result.role ?? "unknown"}`;
case "bootstrap-scope-not-allowed": return `bootstrap profile does not allow scope: ${result.scope ?? "unknown"}`;
}
throw new Error("Unsupported device pairing forbidden reason");
}
function mergeApprovalKind(existing, incoming) {
if (incoming === "owner" || !existing) return incoming;
if (existing.approvedVia === void 0) return incoming === "bootstrap" ? "bootstrap" : void 0;
if (existing.approvedVia === "owner" || existing.approvedVia === "bootstrap") return existing.approvedVia;
return incoming;
}
function buildApprovedPairedDevice(params) {
return {
deviceId: params.pending.deviceId,
publicKey: params.pending.publicKey,
displayName: params.accessMetadata?.displayName ?? params.pending.displayName,
platform: params.pending.platform,
deviceFamily: params.pending.deviceFamily,
clientId: params.pending.clientId,
clientMode: params.pending.clientMode,
browserOrigin: params.pending.browserOrigin,
role: params.pending.role,
roles: params.roles,
scopes: params.approvedScopes,
approvedScopes: params.approvedScopes,
remoteIp: params.accessMetadata?.remoteIp ?? params.pending.remoteIp,
tokens: params.tokens,
approvedVia: mergeApprovalKind(params.existing, params.approvedVia),
...params.existing?.nodeSurface ? { nodeSurface: params.existing.nodeSurface } : {},
...params.existing?.pendingNodeSurface ? { pendingNodeSurface: params.existing.pendingNodeSurface } : {},
...params.existing?.operatorLabel ? { operatorLabel: params.existing.operatorLabel } : {},
createdAtMs: params.existing?.createdAtMs ?? params.now,
approvedAtMs: params.now,
lastSeenAtMs: params.accessMetadata?.lastSeenAtMs ?? params.existing?.lastSeenAtMs,
lastSeenReason: params.accessMetadata?.lastSeenReason ?? params.existing?.lastSeenReason
};
}
function commitApprovedDevicePairing(params) {
const { state, requestId, device, baseDir } = params;
const existing = state.pairedByDeviceId[device.deviceId];
const previousNodeGeneration = resolveNodePairingGeneration(existing ?? null);
const nextNodeGeneration = resolveNodePairingGeneration(device);
const nodePairingGenerationChanged = Boolean(previousNodeGeneration && previousNodeGeneration.key !== nextNodeGeneration?.key);
clearNodePairingGenerationState(device, previousNodeGeneration);
const installationIdentityChanged = Boolean(existing && existing.publicKey !== device.publicKey);
delete state.pendingById[requestId];
state.pairedByDeviceId[device.deviceId] = device;
persistDevicePairingStoreState(state, baseDir, "both", installationIdentityChanged ? { clearApnsNodeIds: [device.deviceId] } : void 0);
invalidatePairedCardRendererCache();
return {
status: "approved",
requestId,
device,
...nodePairingGenerationChanged ? { nodePairingGenerationChanged: true } : {}
};
}
function resolveApprovedTokenScopes(params) {
const pendingScopes = resolveRoleTokenScopes(params.role, params.pending.scopes);
if (pendingScopes.length > 0) {
const approvedBaseline = resolveRoleTokenScopes(params.role, params.existing?.approvedScopes ?? params.existing?.scopes);
const requestedScopeDelta = params.existingToken && approvedBaseline.length > 0 ? pendingScopes.filter((scope) => !approvedBaseline.includes(scope)) : pendingScopes;
if (requestedScopeDelta.length === 0 && params.existingToken) return resolveRoleTokenScopes(params.role, params.existingToken.scopes);
return resolveRoleTokenScopes(params.role, mergeDevicePairingScopes(params.existingToken?.scopes, requestedScopeDelta));
}
return resolveRoleTokenScopes(params.role, params.existingToken?.scopes ?? params.approvedScopes ?? params.existing?.approvedScopes ?? params.existing?.scopes);
}
async function withPendingDevicePairingApproval(requestId, options, baseDir, approve) {
return await withDevicePairingLock(async () => {
const state = await loadDevicePairingState(baseDir);
const pending = state.pendingById[requestId];
if (!pending) return null;
const existing = state.pairedByDeviceId[pending.deviceId];
if (options?.isApprovalCurrent?.({
pending,
existing
}) === false) return {
status: "forbidden",
reason: "approval-policy-changed"
};
return approve(state, pending, existing);
});
}
async function approveDevicePairing(requestId, optionsOrBaseDir, maybeBaseDir) {
const options = typeof optionsOrBaseDir === "string" || optionsOrBaseDir === void 0 ? void 0 : optionsOrBaseDir;
const baseDir = typeof optionsOrBaseDir === "string" ? optionsOrBaseDir : maybeBaseDir;
return await withPendingDevicePairingApproval(requestId, options, baseDir, (state, pendingRecord, existing) => {
const autoApproveScopes = options?.autoApproveNewDeviceScopes;
const requestedRoles = resolveRequestedDeviceRoles(pendingRecord);
const trustedProxySameKeyDevice = options?.approvedVia === "trusted-proxy" && existing !== void 0 && existing.publicKey === pendingRecord.publicKey;
if (autoApproveScopes && ((pendingRecord.isRepair || existing) && !trustedProxySameKeyDevice || !sameDevicePairingStringSet(requestedRoles, [OPERATOR_ROLE]))) return null;
const pending = autoApproveScopes ? {
...pendingRecord,
scopes: [...autoApproveScopes]
} : pendingRecord;
const requestedScopes = normalizeDeviceAuthScopes(pending.scopes);
const roleMismatchScope = resolveScopeOutsideRequestedRoles({
requestedRoles,
requestedScopes
});
if (roleMismatchScope) return {
status: "forbidden",
reason: "scope-outside-requested-roles",
scope: roleMismatchScope
};
const now = Date.now();
const roles = mergeDevicePairingRoles(existing?.roles, existing?.role, pending.roles, pending.role);
const approvedScopes = mergeDevicePairingScopes(existing?.approvedScopes ?? existing?.scopes, pending.scopes);
const tokens = existing?.tokens ? { ...existing.tokens } : {};
const nextTokenScopesByRole = /* @__PURE__ */ new Map();
for (const roleForToken of requestedRoles) {
const existingToken = tokens[roleForToken];
const nextScopes = resolveApprovedTokenScopes({
role: roleForToken,
pending,
existingToken,
approvedScopes,
existing
});
nextTokenScopesByRole.set(roleForToken, nextScopes);
if (roleForToken === OPERATOR_ROLE && nextScopes.length > 0) {
const callerRequiredScopes = mergeDevicePairingScopes(resolveRoleTokenScopes(roleForToken, pending.scopes), nextScopes) ?? nextScopes;
if (!options?.callerScopes) return {
status: "forbidden",
reason: "caller-scopes-required",
scope: callerRequiredScopes[0]
};
const missingScope = resolveMissingRequestedScope({
role: OPERATOR_ROLE,
requestedScopes: callerRequiredScopes,
allowedScopes: options.callerScopes
});
if (missingScope) return {
status: "forbidden",
reason: "caller-missing-scope",
scope: missingScope
};
}
}
for (const [roleForToken, nextScopes] of nextTokenScopesByRole) {
const existingToken = tokens[roleForToken];
const tokenNow = Date.now();
tokens[roleForToken] = {
token: generatePairingToken(),
role: roleForToken,
scopes: nextScopes,
createdAtMs: existingToken?.createdAtMs ?? tokenNow,
rotatedAtMs: existingToken ? tokenNow : void 0,
revokedAtMs: void 0,
lastUsedAtMs: existingToken?.lastUsedAtMs
};
}
return commitApprovedDevicePairing({
state,
requestId,
device: buildApprovedPairedDevice({
pending,
existing,
roles,
approvedScopes,
tokens,
now,
approvedVia: options?.approvedVia ?? "owner",
accessMetadata: options?.accessMetadata
}),
baseDir
});
});
}
async function approveBootstrapDevicePairing(requestId, bootstrapProfile, optionsOrBaseDir, maybeBaseDir) {
const options = typeof optionsOrBaseDir === "string" || optionsOrBaseDir === void 0 ? void 0 : optionsOrBaseDir;
const baseDir = typeof optionsOrBaseDir === "string" ? optionsOrBaseDir : maybeBaseDir;
const approvedRoles = mergeDevicePairingRoles(bootstrapProfile.roles) ?? [];
const approvedScopes = resolveDeviceProfileScopes(bootstrapProfile, approvedRoles);
return await withPendingDevicePairingApproval(requestId, options, baseDir, (state, pending, existing) => {
const requestedRoles = resolveRequestedDeviceRoles(pending);
const missingRole = requestedRoles.find((role) => !approvedRoles.includes(role));
if (missingRole) return {
status: "forbidden",
reason: "bootstrap-role-not-allowed",
role: missingRole
};
const requestedOperatorScopes = normalizeDeviceAuthScopes(pending.scopes).filter((scope) => scope.startsWith(OPERATOR_SCOPE_PREFIX));
const missingScope = resolveMissingRequestedScope({
role: OPERATOR_ROLE,
requestedScopes: requestedOperatorScopes,
allowedScopes: approvedScopes
});
if (missingScope) return {
status: "forbidden",
reason: "bootstrap-scope-not-allowed",
scope: missingScope
};
const now = Date.now();
const grantedRoles = requestedRoles;
const grantedScopes = resolveDeviceProfileScopes(bootstrapProfile, grantedRoles, pending.scopes ?? []);
const grantedRoleSet = new Set(grantedRoles);
const preservedExistingScopes = (mergeDevicePairingRoles(existing?.roles, existing?.role) ?? []).flatMap((existingRole) => grantedRoleSet.has(existingRole) ? [] : preserveDeviceRoleScopes(existingRole, existing?.approvedScopes ?? existing?.scopes));
const roles = mergeDevicePairingRoles(existing?.roles, existing?.role, pending.roles, pending.role);
const nextApprovedScopes = mergeDevicePairingScopes(preservedExistingScopes, grantedScopes);
const tokens = existing?.tokens ? { ...existing.tokens } : {};
for (const roleForToken of grantedRoles) {
const existingToken = tokens[roleForToken];
const tokenScopes = roleForToken === OPERATOR_ROLE ? resolveDeviceProfileRoleScopes(bootstrapProfile, roleForToken, grantedScopes) : [];
tokens[roleForToken] = createDeviceAuthToken({
role: roleForToken,
scopes: tokenScopes,
existing: existingToken,
now,
...existingToken ? { rotatedAtMs: now } : {}
});
}
const device = buildApprovedPairedDevice({
pending,
existing,
roles,
approvedScopes: nextApprovedScopes,
tokens,
now,
approvedVia: "bootstrap",
accessMetadata: options?.accessMetadata
});
const approved = commitApprovedDevicePairing({
state,
requestId,
device,
baseDir
});
const replacedRoles = grantedRoles.filter((role) => existing?.tokens?.[role]);
if (replacedRoles.length > 0) options?.onTokensReplaced?.(device.deviceId, replacedRoles);
return approved;
});
}
//#endregion
export { approveDevicePairing as n, formatDevicePairingForbiddenMessage as r, approveBootstrapDevicePairing as t };