jackwener/OpenCLI · error · ArgumentError

Sales Navigator recipient urn must be urn:li:fs_salesProfile

Error message

Sales Navigator recipient urn must be urn:li:fs_salesProfile:(profileId,authType,authToken)

What it means

An ArgumentError from parseThreadInput (clis/linkedin/salesnav-thread.js:39) raised when the user passes a value that starts with 'urn:li:fs_salesProfile:(' but whose payload does not pass parseSalesProfileUrn validation. A valid URN must be exactly urn:li:fs_salesProfile:(profileId,authType,authToken) with three non-empty, comma-separated parts, none of which are 'undefined', 'null', or 'not_available'. This guards against passing placeholder or truncated URNs that would fail downstream API calls.

Source

Thrown at clis/linkedin/salesnav-thread.js:39

  return host === 'linkedin.com' || host.endsWith('.linkedin.com');
}

function parseSalesProfileUrn(value) {
  const raw = normalizeWhitespace(value);
  const match = raw.match(/^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/);
  if (!match) return '';
  const parts = [match[1], match[2], match[3]].map((part) => normalizeWhitespace(part).toLowerCase());
  if (parts.some((part) => !part || part === 'undefined' || part === 'null' || part === 'not_available')) return '';
  return raw;
}

function parseThreadInput(value) {
  const raw = normalizeWhitespace(value);
  if (!raw) return ['empty', ''];
  if (/^2-[A-Za-z0-9+/=_-]+$/.test(raw)) return ['thread_id', raw];
  if (/^urn:li:fs_salesProfile:\(/.test(raw)) {
    const urn = parseSalesProfileUrn(raw);
    if (!urn) throw new ArgumentError('Sales Navigator recipient urn must be urn:li:fs_salesProfile:(profileId,authType,authToken)');
    return ['recipient_urn', urn];
  }
  try {
    const url = new URL(raw);
    if (url.protocol !== 'https:' || url.username || url.password || url.port || !isLinkedInHost(url.hostname)) return ['name', raw.toLowerCase()];
    const inboxMatch = url.pathname.match(/^\/sales\/inbox\/([^/]+)\/?$/i);
    if (inboxMatch) return ['thread_id', decodeURIComponent(inboxMatch[1])];
    const leadMatch = url.pathname.match(/^\/sales\/lead\/([^,/]+),([^,/]+),([^/]+)\/?$/i);
    if (leadMatch) {
      const urn = `urn:li:fs_salesProfile:(${decodeURIComponent(leadMatch[1])},${decodeURIComponent(leadMatch[2])},${decodeURIComponent(leadMatch[3])})`;
      if (!parseSalesProfileUrn(urn)) {
        throw new ArgumentError('Sales Navigator lead URL must contain resolved profileId, authType, and authToken');
      }
      return ['recipient_urn', urn];
    }
  } catch (err) {
    if (err instanceof ArgumentError) throw err;
    // Fall through to name matching for non-URL text.

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Inspect the URN and ensure it has exactly three comma-separated segments inside parentheses: (profileId,authType,authToken), with no empty or 'undefined' parts.
  2. Copy the recipient_urn fresh from a salesnav-search output row (its recipient_urn column is already validated) instead of hand-building one.
  3. If you only have a profileId, use the Sales Navigator lead URL form https://www.linkedin.com/sales/lead/<profileId>,<authType>,<authToken> or the person's exact name and let the command resolve it.
  4. Strip surrounding whitespace/quotes and re-check for stray characters like nested parentheses or trailing commas before retrying.

Example fix

// before
const urn = `urn:li:fs_salesProfile:(${lead.profileId},undefined,${lead.token})`;
await run('linkedin salesnav-thread', [urn]);
// after (validate parts before building)
const parts = [lead.profileId, lead.authType, lead.authToken];
if (parts.some((p) => !p || p === 'undefined')) throw new Error('incomplete salesProfile parts');
const urn = `urn:li:fs_salesProfile:(${parts.join(',')})`;
await run('linkedin salesnav-thread', [urn]);
Defensive patterns

Strategy: validation

Validate before calling

// Validate a recipient URN before calling salesnav-thread:
function isValidSalesProfileUrn(v) {
  const m = /^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/.test(v);
  const parts = v.slice('urn:li:fs_salesProfile:('.length, -1).split(',');
  return parts.length === 3 && parts.every((p) => p && !['undefined','null','not_available'].includes(p));
}
if (!isValidSalesProfileUrn(urn)) throw new Error('recipient_urn must be (profileId,authType,authToken) with resolved values');

Type guard

function isSalesProfileUrn(value) {
  if (typeof value !== 'string') return false;
  const m = value.match(/^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/);
  if (!m) return false;
  return m.slice(1).every((p) => p && !['undefined', 'null', 'not_available'].includes(p));
}

Try / catch

try {
  await run('linkedin salesnav-thread', [urn]);
} catch (err) {
  if (err instanceof ArgumentError && /recipient urn/.test(err.message)) {
    // fall back to resolving by exact name via salesnav-search output
    return run('linkedin salesnav-thread', [lead.name]);
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing a recipient URN like 'urn:li:fs_salesProfile:(ACoAABC,)' (missing authToken), a URN containing literal 'undefined'/'null'/'not_available' parts copied from unset object fields, or a URN with stray parentheses/extra commas that breaks the strict three-part regex.

Common situations: Copy-pasting a URN from partially rendered JSON or logs where the authToken field was absent; templating a URN from a JS object whose fields were undefined (stringifying to 'undefined'); hand-editing URNs and dropping a segment; older lead_url formats with different segment counts.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/7df82a0af6569324. Report an issue: GitHub.