Files
openclaw/src/gateway/server-methods/device-pair-setup.ts
T
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

101 lines
4.3 KiB
TypeScript

// Device-pairing setup-code method produces the connect QR/setup code a mobile
// or companion client scans to connect to this gateway. It reuses the same
// pairing helpers as `openclaw qr` so non-terminal clients can display the
// connect QR that was previously only renderable in a terminal.
import {
ErrorCodes,
errorShape,
validateDevicePairSetupCodeParams,
} from "../../../packages/gateway-protocol/src/index.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { renderQrPngDataUrl } from "../../media/qr-image.js";
import { encodePairingSetupCode, resolvePairingSetupFromConfig } from "../../pairing/setup-code.js";
import { runCommandWithTimeout } from "../../process/exec.js";
import {
NODE_PAIRING_SETUP_BOOTSTRAP_PROFILE,
PAIRING_SETUP_BOOTSTRAP_PROFILE,
} from "../../shared/device-bootstrap-profile.js";
import { formatForLog } from "../ws-log.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
// Keep the rendered QR within the result schema's qrDataUrl bound. A pathological
// publicUrl can produce a setup code whose QR PNG data URL exceeds the limit; in
// that case we omit the QR (the client can still render one from setupCode)
// rather than return a response that violates the protocol schema.
const MAX_QR_DATA_URL_LENGTH = 16_384;
function readConfiguredDevicePairPublicUrl(config: OpenClawConfig): string | undefined {
const value = config.plugins?.entries?.["device-pair"]?.config?.["publicUrl"];
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
/** Gateway handler for producing a device-pairing setup code + connect QR. */
export const devicePairSetupHandlers: GatewayRequestHandlers = {
"device.pair.setupCode": async ({ params, respond, context }) => {
if (
!assertValidParams(
params,
validateDevicePairSetupCodeParams,
"device.pair.setupCode",
respond,
)
) {
return;
}
try {
const config = context.getRuntimeConfig();
const requestPublicUrl = typeof params.publicUrl === "string" ? params.publicUrl : undefined;
const configuredPublicUrl =
params.preferRemoteUrl === true ? undefined : readConfiguredDevicePairPublicUrl(config);
const publicUrl = requestPublicUrl ?? configuredPublicUrl;
const resolved = await resolvePairingSetupFromConfig(config, {
env: process.env,
publicUrl,
preferRemoteUrl: params.preferRemoteUrl === true,
...(params.bootstrapProfile
? {
bootstrapProfile:
params.bootstrapProfile === "node"
? NODE_PAIRING_SETUP_BOOTSTRAP_PROFILE
: PAIRING_SETUP_BOOTSTRAP_PROFILE,
}
: {}),
// Lets Tailscale serve/funnel URLs resolve, mirroring the `openclaw qr` CLI.
runCommandWithTimeout: async (argv, runOpts) =>
await runCommandWithTimeout(argv, { timeoutMs: runOpts.timeoutMs }),
});
if (!resolved.ok) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, resolved.error));
return;
}
const setupCode = encodePairingSetupCode(resolved.payload);
// QR is on by default; callers that only need the code can opt out.
const includeQr = params.includeQr !== false;
// QR rendering is optional output; keep the usable setup code if encoding fails.
const renderedQr = includeQr
? await renderQrPngDataUrl(setupCode).catch(() => undefined)
: undefined;
const qrDataUrl =
renderedQr && renderedQr.length <= MAX_QR_DATA_URL_LENGTH ? renderedQr : undefined;
respond(
true,
{
setupCode,
...(qrDataUrl ? { qrDataUrl } : {}),
gatewayUrl: resolved.payload.url,
...(resolved.payload.urls ? { gatewayUrls: resolved.payload.urls } : {}),
// Label only — never the raw gateway token/password.
auth: resolved.authLabel,
urlSource: requestPublicUrl ? "request.publicUrl" : resolved.urlSource,
access: resolved.access,
...(resolved.accessDowngraded ? { accessDowngraded: true } : {}),
},
undefined,
);
} catch (err) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
}
},
};