UNPKG

3.15 kBJavaScriptView Raw
1'use strict';
2
3const Hasha = require('hasha');
4const Assert = require('assert');
5const Crypto = require('crypto');
6const QueryString = require('querystring');
7const Bounce = require('bounce');
8const Wreck = require('wreck');
9const Boom = require('boom');
10
11module.exports = class CloudApi {
12 constructor ({ token, url, keyId, key, log } = {}) {
13 const env = process.env.NODE_ENV;
14 Assert(token || env === 'development' || env === 'test', 'token is required for production');
15
16 this._token = token;
17 this._keyId = keyId;
18 this._key = key;
19 this._cache = {};
20 this._wreck = Wreck.defaults({
21 headers: this._authHeaders(),
22 baseUrl: `${url}/my`,
23 json: true
24 });
25 this._log = log.bind(this);
26 this.fetch = this.fetch.bind(this);
27 }
28
29 _authHeaders () {
30 const now = new Date().toUTCString();
31 const signer = Crypto.createSign('sha256');
32 signer.update(now);
33 const signature = signer.sign(this._key, 'base64');
34
35 const headers = {
36 'Content-Type': 'application/json',
37 Date: now,
38 Authorization: `Signature keyId="${
39 this._keyId
40 }",algorithm="rsa-sha256" ${signature}`
41 };
42
43 if (this._token) {
44 headers['X-Auth-Token'] = this._token;
45 }
46
47 return headers;
48 }
49
50 _getCache (method = '', path, options) {
51 if (method.toLowerCase() !== 'get') {
52 return;
53 }
54
55 const ref = Hasha(JSON.stringify({ method, path, options }));
56 const { val, when } = this._cache[ref] || {};
57 const now = new Date().getTime();
58
59 if (!when) {
60 return;
61 }
62
63 // cache is no logner fresh
64 if (now - when > 9000) {
65 delete this._cache[ref];
66 return val;
67 }
68
69 return val;
70 }
71
72 _setCache (method = '', path, options, payload) {
73 if (method.toLowerCase() !== 'get') {
74 return;
75 }
76
77 const ref = Hasha(JSON.stringify({ method, path, options }));
78 this._cache[ref] = {
79 when: new Date().getTime(),
80 val: payload
81 };
82 }
83
84 async _request (path = '/', options = {}) {
85 const wreckOptions = {
86 json: true,
87 payload: options.payload,
88 headers: options.headers
89 };
90
91 if (options.query) {
92 path += `?${QueryString.stringify(options.query)}`;
93 }
94
95 const method = (options.method && options.method.toLowerCase()) || 'get';
96
97 const cached = this._getCache(method, path, wreckOptions);
98 if (cached) {
99 return cached;
100 }
101
102 try {
103 const results = await this._wreck[method](path, wreckOptions);
104 this._setCache(method, path, wreckOptions, results);
105 return results;
106 } catch (ex) {
107 this._log(['error', path], (ex.data && ex.data.payload) || ex);
108 Bounce.rethrow(ex, 'system');
109
110 if (options.default !== undefined) {
111 return { payload: options.default, res: {} };
112 }
113
114 if (ex.data && ex.data.payload && ex.data.payload.message) {
115 throw new Boom(ex.data.payload.message, ex.output.payload);
116 }
117
118 throw ex;
119 }
120 }
121
122 async fetch (path = '/', options = {}) {
123 const { payload, res } = await this._request(path, options);
124 return options.includeRes ? { payload, res } : payload;
125 }
126};