santifer/career-ops · error · Error
mokahr: response missing data/necromancer — not the…
Error message
mokahr: response missing data/necromancer — not the expected envelope shape
What it means
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.
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.
Example fix
// before
const envelope = await res.json();
const decrypted = decryptMokaHrEnvelope(envelope);
// after
const envelope = await res.json();
if (!envelope?.data || !envelope?.necromancer) {
console.error('mokahr: no envelope, body was:', body.slice(0, 200));
}
const decrypted = decryptMokaHrEnvelope(envelope); Defensive patterns
Strategy: try-catch
Validate before calling
const hasEnvelope = (env) => Boolean(env && typeof env === 'object' && env.data && env.necromancer);
Type guard
const isMokaEnvelope = (v) => typeof v === 'object' && v !== null && typeof v.data === 'string' && typeof v.necromancer === 'string';
Try / catch
try {
decrypted = decryptMokaHrEnvelope(envelope);
} catch (err) {
if (String(err.message).includes('missing data/necromancer')) {
console.error('MokaHR returned a non-envelope body — likely a block page or error JSON:', rawBody.slice(0, 200));
return partialResults;
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- 4dayweek: unexpected API response on page
- a16z-speedrun-talent: unexpected API response on page
- agentic-jobs: unexpected API response shape on page
- API error
- arbeitnow: unexpected API response on page
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/00f853405cce51bb.
Report an issue: GitHub.
Appendix: source
Thrown at providers/mokahr.mjs:129
const m = TENANT_PATH_RE.exec(u.pathname);
if (!m) return null;
const siteId = Number(m[2]);
if (!Number.isSafeInteger(siteId) || siteId <= 0) return null;
const pathname = u.pathname.replace(/\/$/, '');
if (ROBOTS_EXCLUDED_PATHS.has(pathname)) return null;
return { orgId: m[1], siteId, baseUrl: `${u.origin}${pathname}` };
}
/**
* Decrypt one `{data, necromancer}` envelope into the plaintext response.
* Exported for tests — deliberately separate from the HTTP call so tests
* never need a real network round-trip to exercise the crypto.
* @param {{ data?: string, necromancer?: string }} envelope
* @returns {any}
*/
export function decryptMokaHrEnvelope(envelope) {
if (!envelope?.data || !envelope?.necromancer) {
throw new Error('mokahr: response missing data/necromancer — not the expected envelope shape');
}
const key = Buffer.from(envelope.necromancer, 'utf8');
if (key.length !== 16) {
throw new Error(`mokahr: necromancer key is ${key.length} bytes, expected 16 (aes-128-cbc)`);
}
const ciphertext = Buffer.from(envelope.data, 'base64');
const decipher = createDecipheriv('aes-128-cbc', key, AES_IV);
const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return JSON.parse(plain.toString('utf8'));
}
/**
* @param {any} decrypted - Already-decrypted response body.
* @param {string} companyName
* @param {string} tenantBaseUrl - Validated tenant careers URL without a trailing slash.
* @returns {import('./_types.js').Job[]}
*/
export function parseMokaHrJobs(decrypted, companyName, tenantBaseUrl) {View on GitHub (pinned to aac998c7ed)