{"record":{"id":"00f853405cce51bb","repo":"santifer/career-ops","slug":"mokahr-response-missing-data-necromancer-not-th","errorCode":null,"errorMessage":"mokahr: response missing data/necromancer — not the expected envelope shape","messagePattern":"mokahr: response missing data/necromancer — not the expected envelope shape","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/mokahr.mjs","lineNumber":129,"sourceCode":"  const m = TENANT_PATH_RE.exec(u.pathname);\n  if (!m) return null;\n  const siteId = Number(m[2]);\n  if (!Number.isSafeInteger(siteId) || siteId <= 0) return null;\n  const pathname = u.pathname.replace(/\\/$/, '');\n  if (ROBOTS_EXCLUDED_PATHS.has(pathname)) return null;\n  return { orgId: m[1], siteId, baseUrl: `${u.origin}${pathname}` };\n}\n\n/**\n * Decrypt one `{data, necromancer}` envelope into the plaintext response.\n * Exported for tests — deliberately separate from the HTTP call so tests\n * never need a real network round-trip to exercise the crypto.\n * @param {{ data?: string, necromancer?: string }} envelope\n * @returns {any}\n */\nexport function decryptMokaHrEnvelope(envelope) {\n  if (!envelope?.data || !envelope?.necromancer) {\n    throw new Error('mokahr: response missing data/necromancer — not the expected envelope shape');\n  }\n  const key = Buffer.from(envelope.necromancer, 'utf8');\n  if (key.length !== 16) {\n    throw new Error(`mokahr: necromancer key is ${key.length} bytes, expected 16 (aes-128-cbc)`);\n  }\n  const ciphertext = Buffer.from(envelope.data, 'base64');\n  const decipher = createDecipheriv('aes-128-cbc', key, AES_IV);\n  const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n  return JSON.parse(plain.toString('utf8'));\n}\n\n/**\n * @param {any} decrypted - Already-decrypted response body.\n * @param {string} companyName\n * @param {string} tenantBaseUrl - Validated tenant careers URL without a trailing slash.\n * @returns {import('./_types.js').Job[]}\n */\nexport function parseMokaHrJobs(decrypted, companyName, tenantBaseUrl) {","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/mokahr.mjs#L111-L147","documentation":"MokaHR encrypts its API responses; decryptMokaHrEnvelope() expects an envelope object with two fields: data (base64 ciphertext) and necromancer (the encryption key string). If either field is missing or empty, this error is thrown before any crypto runs. It means the HTTP response did not have the encrypted envelope shape the provider requires.","triggerScenarios":"The MokaHR endpoint returned a body without data/necromancer — e.g. a plain JSON error object, an HTML/login page parsed as text, a rate-limit message, or an API version change that renamed the fields.","commonSituations":"Bot protection or a WAF serves a challenge page instead of the API; the tenant site ID is wrong so the endpoint returns a different error shape; MokaHR changes its response envelope; the request was unauthenticated where encryption is expected.","solutions":["Log the raw response body for the failing request and inspect what actually came back.","Verify the tenant careers_url/site ID is correct — a bad tenant often yields a different response shape.","Check whether MokaHR changed the envelope field names and update decryptMokaHrEnvelope accordingly.","If bot protection is the cause, slow request rate or adjust headers; if auth is required, supply the needed credentials/cookies via the fetch context."],"exampleFix":"// before\nconst envelope = await res.json();\nconst decrypted = decryptMokaHrEnvelope(envelope);\n// after\nconst envelope = await res.json();\nif (!envelope?.data || !envelope?.necromancer) {\n  console.error('mokahr: no envelope, body was:', body.slice(0, 200));\n}\nconst decrypted = decryptMokaHrEnvelope(envelope);","handlingStrategy":"try-catch","validationCode":"const hasEnvelope = (env) => Boolean(env && typeof env === 'object' && env.data && env.necromancer);","typeGuard":"const isMokaEnvelope = (v) => typeof v === 'object' && v !== null && typeof v.data === 'string' && typeof v.necromancer === 'string';","tryCatchPattern":"try {\n  decrypted = decryptMokaHrEnvelope(envelope);\n} catch (err) {\n  if (String(err.message).includes('missing data/necromancer')) {\n    console.error('MokaHR returned a non-envelope body — likely a block page or error JSON:', rawBody.slice(0, 200));\n    return partialResults;\n  }\n  throw err;\n}","preventionTips":["Capture and log the raw HTTP body whenever decryption fails, to distinguish WAF pages from API changes.","Verify tenant site IDs before scanning — bad tenants return different shapes.","Keep an integration test that exercises decryptMokaHrEnvelope with a recorded real envelope.","Rate-limit requests so bot protection does not replace the envelope with a challenge."],"tags":["api","crypto","response-shape","mokahr"],"backgroundTag":"unexpected-response-shape","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}