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 1921x 1921x 1921x 9x 9x 9x 9x 9x 9x 301x 301x 301x 301x 891x 891x 891x 881x 881x 301x 301x 301x 301x 301x 9x 9x 9x 291x 291x 9x 9x 9x 9x 9x 9x 372x 372x 11x 11x 372x 355x 355x 6x 372x 9x 9x 272x 272x 9x 9x 9x 131x 125x 125x 125x 125x 131x 131x 131x 9x 9x 9x 151x 151x 151x 151x 156x 156x 151x 4x 4x 4x 147x 147x 147x 147x 147x 151x  
// 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;
}