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

  1. Log Buffer.byteLength(envelope.necromancer) and the first bytes of the key to see what arrived.
  2. Confirm the envelope you passed in is the raw API response, not a re-parsed/wrapped object where necromancer picked up the wrong field.
  3. If MokaHR moved to 32-byte keys, switch the cipher to aes-256-cbc and the IV length accordingly in decryptMokaHrEnvelope.
  4. 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

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


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)