All files signer.js

100% Statements 82/82
90.24% Branches 37/41
100% Functions 7/7
100% Lines 82/82

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 839x 9x 9x 9x 9x 1856x 1856x 1856x 9x 9x 9x 9x 9x 9x 291x 291x 291x 291x 861x 861x 861x 851x 851x 291x 291x 291x 291x 291x 9x 9x 9x 281x 281x 9x 9x 9x 9x 9x 9x 369x 369x 11x 11x 369x 352x 352x 6x 369x 9x 9x 262x 262x 9x 9x 9x 126x 120x 120x 120x 120x 126x 126x 126x 9x 9x 9x 146x 146x 146x 146x 151x 151x 146x 4x 4x 4x 142x 142x 142x 142x 142x 146x  
// Request signing (CONTRACT 0.2, DESIGN 5.2) and authentication headers or query parameters.
import { hmacSha256, base64Encode, utf8Encode } from './codec.js';
import { localError } from './errors.js';
import { isNil, randomHex } from './util.js';
 
function text(v) {
  return typeof v === 'string' ? v : String(v);
}
 
/**
 * Signed payload: keys in UTF-16 code unit order (Java TreeMap order), null and empty values dropped,
 * `k=v` joined with `&`, no URL encoding.
 */
export function buildSignPayload(params) {
  const p = params || {};
  const keys = [];
  const all = Object.keys(p);
  for (let i = 0; i < all.length; i++) {
    const v = p[all[i]];
    if (isNil(v)) continue;
    if (text(v) === '') continue;
    keys.push(all[i]);
  }
  keys.sort();
  const parts = [];
  for (let i = 0; i < keys.length; i++) parts.push(keys[i] + '=' + text(p[keys[i]]));
  return parts.join('&');
}
 
/** Base64(HMAC_SHA256(payload, secretKey)) over UTF-8 bytes. */
export function signParams(params, secretKey) {
  return base64Encode(hmacSha256(utf8Encode(String(secretKey)), utf8Encode(buildSignPayload(params))));
}
 
/**
 * Resolves the auth option. Accepts `{auth: {token}}`, `{auth: {appKey, secretKey}}` and, for 1.x
 * code, the same fields at the top level of the client options.
 */
export function createAuth(options) {
  const a = options && options.auth && typeof options.auth === 'object' ? options.auth : options || {};
  if (typeof a.token === 'string' && a.token) {
    return Object.freeze({ mode: 'token', token: a.token, secrets: [a.token] });
  }
  if (typeof a.appKey === 'string' && a.appKey && typeof a.secretKey === 'string' && a.secretKey) {
    return Object.freeze({ mode: 'sign', appKey: a.appKey, secretKey: a.secretKey, secrets: [a.secretKey] });
  }
  throw localError(90010, 'auth is required: {token} or {appKey, secretKey}');
}
 
export function nowSeconds(nowMs) {
  return String(Math.floor((typeof nowMs === 'number' ? nowMs : Date.now()) / 1000));
}
 
/** REST headers. Timestamp and nonce are fresh for every attempt, the signature covers signSet only. */
export function restAuthHeaders(auth, signSet, nowMs) {
  if (auth.mode === 'token') return { Authorization: 'Bearer ' + auth.token };
  return {
    'X-App-Key': auth.appKey,
    'X-Timestamp': nowSeconds(nowMs),
    'X-Nonce': randomHex(8),
    'X-Signature': signParams(signSet || {}, auth.secretKey),
  };
}
 
/** WS handshake query. The signature covers every query parameter except itself. */
export function wsAuthQuery(auth, query, nowMs) {
  const q = {};
  const src = query || {};
  const keys = Object.keys(src);
  for (let i = 0; i < keys.length; i++) {
    if (!isNil(src[keys[i]]) && src[keys[i]] !== '') q[keys[i]] = text(src[keys[i]]);
  }
  if (auth.mode === 'token') {
    q.token = auth.token;
    return q;
  }
  q.appKey = auth.appKey;
  q.timestamp = nowSeconds(nowMs);
  q.nonce = randomHex(8);
  q.signature = signParams(q, auth.secretKey);
  return q;
}