Files
openclaw/extensions/device-pair/pair-command-auth.ts
Peter Steinberger 938d41014f feat(mobile): default pairing to full node access (#105928)
* feat(mobile): default pairing to full node access

* chore: leave release notes to release workflow

* refactor(mobile): bound pairing helpers

* chore(i18n): refresh mobile access inventory

* chore(ci): lower mobile pairing LOC baseline

* fix(mobile): complete pairing validation artifacts

* ci: remove stale transcript target export baseline

* fix(ios): wrap access upgrade guidance

* fix(ios): preserve localized access key

* fix(ci): dedupe read-only SQLite guardrail

* fix(mobile): secure full-access node setup
2026-07-13 02:30:45 -07:00

80 lines
2.5 KiB
TypeScript

// Device Pair plugin module implements pair command auth behavior.
type PairingCommandAuthParams = {
channel: string;
gatewayClientScopes?: readonly string[] | null;
senderIsOwner?: boolean;
};
type PairingCommandAuthState = {
isInternalGatewayCaller: boolean;
isMissingPairingPrivilege: boolean;
isMissingSetupHandoffPrivilege: boolean;
canIssueFullAccessSetup: boolean;
approvalCallerScopes?: readonly string[];
};
const COMMAND_OWNER_PAIRING_SCOPES = ["operator.pairing"] as const;
const PAIRING_SCOPE = "operator.pairing";
const ADMIN_SCOPE = "operator.admin";
const TALK_SECRETS_SCOPE = "operator.talk.secrets";
function isInternalGatewayPairingCaller(params: PairingCommandAuthParams): boolean {
return params.channel === "webchat" || Array.isArray(params.gatewayClientScopes);
}
function hasPairingPrivilege(scopes: readonly string[]): boolean {
return scopes.includes(PAIRING_SCOPE) || scopes.includes(ADMIN_SCOPE);
}
function hasSetupHandoffPrivilege(scopes: readonly string[]): boolean {
return scopes.includes(TALK_SECRETS_SCOPE) || scopes.includes(ADMIN_SCOPE);
}
export function resolvePairingCommandAuthState(
params: PairingCommandAuthParams,
): PairingCommandAuthState {
const isInternalGatewayCaller = isInternalGatewayPairingCaller(params);
if (isInternalGatewayCaller) {
const approvalCallerScopes = Array.isArray(params.gatewayClientScopes)
? params.gatewayClientScopes
: [];
return {
isInternalGatewayCaller,
isMissingPairingPrivilege: !hasPairingPrivilege(approvalCallerScopes),
isMissingSetupHandoffPrivilege: !hasSetupHandoffPrivilege(approvalCallerScopes),
canIssueFullAccessSetup: approvalCallerScopes.includes(ADMIN_SCOPE),
approvalCallerScopes,
};
}
if (params.senderIsOwner === true) {
return {
isInternalGatewayCaller,
isMissingPairingPrivilege: false,
isMissingSetupHandoffPrivilege: false,
canIssueFullAccessSetup: true,
approvalCallerScopes: COMMAND_OWNER_PAIRING_SCOPES,
};
}
return {
isInternalGatewayCaller,
isMissingPairingPrivilege: true,
isMissingSetupHandoffPrivilege: true,
canIssueFullAccessSetup: false,
approvalCallerScopes: undefined,
};
}
export function buildMissingPairingScopeReply(): { text: string } {
return {
text: "⚠️ This command requires operator.pairing.",
};
}
export function buildMissingSetupHandoffScopeReply(): { text: string } {
return {
text: "⚠️ Setup code handoff includes Talk secrets and requires operator.talk.secrets.",
};
}