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 1912x 1912x 1912x 9x 9x 9x 9x 9x 9x 300x 300x 300x 300x 887x 887x 887x 877x 877x 300x 300x 300x 300x 300x 9x 9x 9x 290x 290x 9x 9x 9x 9x 9x 9x 372x 372x 11x 11x 372x 355x 355x 6x 372x 9x 9x 271x 271x 9x 9x 9x 131x 125x 125x 125x 125x 131x 131x 131x 9x 9x 9x 150x 150x 150x 150x 155x 155x 150x 4x 4x 4x 146x 146x 146x 146x 146x 150x  
// 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;
}