d58f53525812e9ff5e4790972e4936ceb7dd743c
[squeep-indie-auther] / src / common.js
1 'use strict';
2
3 const { common } = require('@squeep/api-dingus');
4
5 const { randomBytes } = require('crypto');
6 const { promisify } = require('util');
7 const randomBytesAsync = promisify(randomBytes);
8
9 /**
10 * Pick out useful axios response fields.
11 * @param {*} res
12 * @returns
13 */
14 const axiosResponseLogData = (res) => {
15 const data = common.pick(res, [
16 'status',
17 'statusText',
18 'headers',
19 'elapsedTimeMs',
20 'data',
21 ]);
22 if (data.data) {
23 data.data = logTruncate(data.data, 100);
24 }
25 return data;
26 };
27
28 /**
29 * Limit length of string to keep logs sane
30 * @param {String} str
31 * @param {Number} len
32 * @returns {String}
33 */
34 const logTruncate = (str, len) => {
35 if (typeof str !== 'string' || str.toString().length <= len) {
36 return str;
37 }
38 return str.toString().slice(0, len) + `... (${str.toString().length} bytes)`;
39 };
40
41 /**
42 * Turn a snake into a camel.
43 * @param {String} snakeCase
44 * @param {String|RegExp} delimiter
45 * @returns {String}
46 */
47 const camelfy = (snakeCase, delimiter = '_') => {
48 if (!snakeCase || typeof snakeCase.split !== 'function') {
49 return undefined;
50 }
51 const words = snakeCase.split(delimiter);
52 return [
53 words.shift(),
54 ...words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)),
55 ].join('');
56 };
57
58 /**
59 * Return an array containing x if x is not an array.
60 * @param {*} x
61 */
62 const ensureArray = (x) => {
63 if (x === undefined) {
64 return [];
65 }
66 if (!Array.isArray(x)) {
67 return Array(x);
68 }
69 return x;
70 };
71
72 /**
73 * Recursively freeze an object.
74 * @param {Object} o
75 * @returns {Object}
76 */
77 const freezeDeep = (o) => {
78 Object.freeze(o);
79 Object.getOwnPropertyNames(o).forEach((prop) => {
80 if (Object.hasOwnProperty.call(o, prop)
81 && ['object', 'function'].includes(typeof o[prop]) // eslint-disable-line security/detect-object-injection
82 && !Object.isFrozen(o[prop])) { // eslint-disable-line security/detect-object-injection
83 return freezeDeep(o[prop]); // eslint-disable-line security/detect-object-injection
84 }
85 });
86 return o;
87 };
88
89
90 /** Oauth2.1 §3.2.3.1
91 * %x20-21 / %x23-5B / %x5D-7E
92 * @param {String} char
93 */
94 const validErrorChar = (char) => {
95 const value = char.charCodeAt(0);
96 return value === 0x20 || value === 0x21
97 || (value >= 0x23 && value <= 0x5b)
98 || (value >= 0x5d && value <= 0x7e);
99 };
100
101
102 /**
103 * Determine if an OAuth error message is valid.
104 * @param {String} error
105 * @returns {Boolean}
106 */
107 const validError = (error) => {
108 return error && error.split('').filter((c) => !validErrorChar(c)).length === 0 || false;
109 };
110
111
112 /**
113 * OAuth2.1 §3.2.2.1
114 * scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
115 * @param {String} char
116 */
117 const validScopeChar = (char) => {
118 const value = char.charCodeAt(0);
119 return value === 0x21
120 || (value >= 0x23 && value <= 0x5b)
121 || (value >= 0x5d && value <= 0x7e);
122 };
123
124
125 /**
126 * Determine if a scope has a valid name.
127 * @param {String} scope
128 * @returns {Boolean}
129 */
130 const validScope = (scope) => {
131 return scope && scope.split('').filter((c) => !validScopeChar(c)).length === 0 || false;
132 };
133
134
135 /**
136 *
137 * @param {Number} bytes
138 */
139 const newSecret = async (bytes = 64) => {
140 return (await randomBytesAsync(bytes * 3 / 4)).toString('base64');
141 };
142
143
144 /**
145 * Convert a Date object to epoch seconds.
146 * @param {Date=} date
147 * @returns {Number}
148 */
149 const dateToEpoch = (date) => {
150 const dateMs = date ? date.getTime() : Date.now();
151 return Math.ceil(dateMs / 1000);
152 };
153
154 module.exports = {
155 ...common,
156 axiosResponseLogData,
157 camelfy,
158 dateToEpoch,
159 ensureArray,
160 freezeDeep,
161 logTruncate,
162 newSecret,
163 randomBytesAsync,
164 validScope,
165 validError,
166 };
167