{"record":{"id":"7ae355a3a5002fdd","repo":"santifer/career-ops","slug":"apify-invalid-actorid-json-stringify-actorid","errorCode":null,"errorMessage":"apify: invalid actorId ${JSON.stringify(actorId)}. Expected \"owner/actor\" or \"owner~actor\" with letters, digits, \"_\", \".\", or \"-\" only.","messagePattern":"apify: invalid actorId (.+?)\\. Expected \"owner/actor\" or \"owner~actor\" with letters, digits, \"_\", \"\\.\", or \"-\" only\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/apify/_apify.mjs","lineNumber":39,"sourceCode":"const DEFAULT_RUN_TIMEOUT_MS = 180_000;\nconst POLL_INTERVAL_MS = 3_000;\nconst PER_REQUEST_TIMEOUT_MS = 15_000;\nconst CONNECT_RETRY_ATTEMPTS = 3;\nconst TERMINAL_STATUSES = new Set(['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT']);\n\nexport function hasToken(token = process.env.APIFY_TOKEN) {\n  return Boolean(token);\n}\n\n// Apify accepts both \"user/actor\" and \"user~actor\" in URLs; normalize to `~`.\n// Validate strictly so a malformed config can't escape the intended\n// /acts/<actor>/runs path with extra `/`, `..`, `?`, or `#` characters and\n// send our bearer token to an unintended endpoint on api.apify.com.\nconst ACTOR_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$/;\n\nexport function normalizeActorId(actorId) {\n  if (typeof actorId !== 'string' || !ACTOR_ID_RE.test(actorId)) {\n    throw new Error(\n      `apify: invalid actorId ${JSON.stringify(actorId)}. ` +\n      `Expected \"owner/actor\" or \"owner~actor\" with letters, digits, \"_\", \".\", or \"-\" only.`\n    );\n  }\n  const [owner, name] = actorId.split(/[~/]/, 2);\n  return `${encodeURIComponent(owner)}~${encodeURIComponent(name)}`;\n}\n\n// Apify supports auth via ?token= or Authorization: Bearer. The query-string\n// form leaks the token into HTTP access logs and any error/log line that\n// includes the URL, so always use the header.\nfunction authHeaders(token) {\n  return { authorization: `Bearer ${token}` };\n}\n\nfunction sleep(ms) {\n  return new Promise(r => setTimeout(r, ms));\n}","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/apify/_apify.mjs#L21-L57","documentation":"Thrown by `normalizeActorId` (plugins/apify/_apify.mjs:39) when the actorId does not match the strict regex `^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$`. Apify actor IDs use the `owner/actor` or `owner~actor` form; strict validation prevents a malformed config from injecting extra `/`, `..`, `?`, or `#` that could escape the intended `/acts/<actor>/runs` path and send the bearer token to an unintended endpoint on api.apify.com. This is a security guard, not just input hygiene.","triggerScenarios":"`runActor(actorId, ...)` or `startRun` is called with an actorId that is not a string, missing the owner/actor separator, contains disallowed characters (slash variants, spaces, path traversal), or has an empty owner/actor segment. The regex is tested before any URL is built.","commonSituations":"portals.yml entry has `actor: indeed-scraper` (missing owner); `actor: misceres/indeed-scraper/extra` (extra slash); `actor: ../admin`; a copy-paste that included a trailing slash or query string; passing a full Apify URL instead of the actor ID.","solutions":["Use the canonical `owner/actor` or `owner~actor` form, e.g. `misceres/indeed-scraper`.","Strip any leading/trailing whitespace, slashes, or query strings before passing the actorId.","Copy the actor ID directly from the Apify store URL path (the segment after /acts/).","If you only have a URL like https://apify.com/store/acts/misceres/indeed-scraper, take the `misceres/indeed-scraper` portion."],"exampleFix":"# before — portals.yml\n- name: indeed\n  provider: apify\n  actor: indeed-scraper          # missing owner → invalid\n# after\n- name: indeed\n  provider: apify\n  actor: misceres/indeed-scraper","handlingStrategy":"validation","validationCode":"import { normalizeActorId } from './plugins/apify/_apify.mjs';\n// Validate the actorId shape before persisting it in portals.yml.\nfunction isValidActorId(id) {\n  try { normalizeActorId(id); return true; }\n  catch { return false; }\n}\nfor (const e of portals.filter(p => p.provider === 'apify')) {\n  if (!isValidActorId(e.actor)) throw new Error(`Entry '${e.name}' has a malformed apify actor: '${e.actor}'`);\n}","typeGuard":"const ACTOR_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$/;\n/** @param {unknown} v */\nfunction isValidActorId(v) {\n  return typeof v === 'string' && ACTOR_ID_RE.test(v);\n}","tryCatchPattern":"try {\n  await runActor(entry.actor, entry.input, opts);\n} catch (err) {\n  if (/invalid actorId/.test(err.message)) {\n    console.error(`Fix the actor id in portals.yml: ${err.message}`);\n    process.exitCode = 2;\n  } else throw err;\n}","preventionTips":["Lint apify `actor` fields in portals.yml with the same regex in CI.","Copy actor IDs verbatim from the Apify store URL path."],"tags":["apify","validation","security","path-traversal","configuration"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}