paperclipai/paperclip · error · AuthorizationError

unauthorized_peer

unauthorized_peer

Error message

missing peer credentials

What it means

AuthorizationError('unauthorized_peer'/'missing peer credentials') thrown by authorizePeer, the complete-mediation check run on every accepted broker connection before any request is decoded. The peer credential structure lacked usable uid/gid/pid values, so SO_PEERCRED identity could not be established and the connection is denied rather than treated as trusted.

Solutions

  1. Connect via Tailscale HTTPS so peer credentials are present, or use an authorized local path.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at packages/tailscale-https-broker/src/authorization.ts:43 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/34da16264f74d4ed. Report an issue: GitHub.

Appendix: source

Thrown at packages/tailscale-https-broker/src/authorization.ts:43

      | "unauthorized_peer"
      | "invalid_handle"
      | "listener_ownership_mismatch",
    message: string,
  ) {
    super(message);
    this.name = "AuthorizationError";
  }
}

/**
 * Complete-mediation check run on every accepted connection before any request
 * is even decoded. Throws AuthorizationError("unauthorized_peer") on any
 * mismatch. Supplemental-group-only membership does not satisfy the GID check
 * because peer.gid is the process's primary GID from SO_PEERCRED.
 */
export function authorizePeer(peer: PeerCredentials, policy: PeerPolicy): void {
  if (!Number.isInteger(peer.uid) || !Number.isInteger(peer.gid)) {
    throw new AuthorizationError("unauthorized_peer", "missing peer credentials");
  }
  if (!policy.allowedUids.has(peer.uid)) {
    throw new AuthorizationError("unauthorized_peer", `uid ${peer.uid} not allowlisted`);
  }
  if (!policy.allowedGids.has(peer.gid)) {
    throw new AuthorizationError("unauthorized_peer", `gid ${peer.gid} not allowlisted`);
  }
}

/** Generate an unguessable lease handle (256 bits, url-safe). */
export function generateLeaseHandle(): string {
  return randomBytes(32).toString("base64url");
}

/** Constant-time handle comparison to avoid timing oracles. */
export function handlesEqual(a: string, b: string): boolean {
  const ab = Buffer.from(a, "utf8");
  const bb = Buffer.from(b, "utf8");

View on GitHub (pinned to 120ae5428f)