santifer/career-ops · error · Error
mokahr: necromancer key is
Error message
mokahr: necromancer key is ${key.length} bytes, expected 16 (aes-128-cbc) What it means
The necromancer field is the raw AES-128 key and must be exactly 16 bytes of UTF-8 for aes-128-cbc. decryptMokaHrEnvelope() measures the Buffer length and throws this error when it differs. A wrong-length key means createDecipheriv would otherwise fail or the API changed its key format.
Solutions
- Log Buffer.byteLength(envelope.necromancer) and the first bytes of the key to see what arrived.
- Confirm the envelope you passed in is the raw API response, not a re-parsed/wrapped object where necromancer picked up the wrong field.
- If MokaHR moved to 32-byte keys, switch the cipher to aes-256-cbc and the IV length accordingly in decryptMokaHrEnvelope.
- If the key is base64/hex encoded, decode it to raw bytes before measuring/using it.
Example fix
// before
const key = Buffer.from(envelope.necromancer, 'utf8');
const decipher = createDecipheriv('aes-128-cbc', key, AES_IV);
// after (if the API now sends a 32-byte key)
const key = Buffer.from(envelope.necromancer, 'utf8');
const algo = key.length === 16 ? 'aes-128-cbc' : 'aes-256-cbc';
const decipher = createDecipheriv(algo, key, AES_IV.slice(0, algo === 'aes-256-cbc' ? 16 : AES_IV.length)); Defensive patterns
Strategy: validation
Type guard
const isAes128Key = (s) => typeof s === 'string' && Buffer.byteLength(s, 'utf8') === 16;
Try / catch
try {
decrypted = decryptMokaHrEnvelope(envelope);
} catch (err) {
if (String(err.message).includes('expected 16')) {
console.error(`Key length was wrong; necromancer bytes=${Buffer.byteLength(envelope?.necromancer ?? '', 'utf8')}`);
return partialResults;
}
throw err;
} Prevention
- Pass the raw API envelope straight into decryptMokaHrEnvelope — never re-encode the necromancer field first.
- Log key byte length on failure to spot AES-256 migrations quickly.
- Record a known-good envelope in tests so key-format changes fail CI, not production scans.
- Do not base64/hex-decode the key unless the provider code says to.
When it happens
Trigger: The decrypted envelope's necromancer string is empty, truncated, base64/hex-encoded when the code expects raw UTF-8, or a longer/shorter key introduced by an API change — any UTF-8 encoding that is not exactly 16 bytes.
Common situations: MokaHR rotates to a 32-byte (AES-256) key without the provider being updated; the envelope was partially parsed so a nested/wrong field was passed as the key; double-decoding the field (e.g. Buffer.from(x,'base64') first) changes its byte length.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- mokahr: response missing data/necromancer — not the…
- API error
- mokahr: careers_url must be an allowed HTTPS app.mokahr.com…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/e84b01147aaab8ce.
Report an issue: GitHub.
Appendix: source
Thrown at providers/mokahr.mjs:133
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) {
const list = decrypted?.data?.jobs;
if (!Array.isArray(list)) return [];
const jobs = [];View on GitHub (pinned to aac998c7ed)