update dependencies and devDependencies, address lint issues
[squeep-indie-auther] / src / common.js
1 'use strict';
2
3 const { common } = require('@squeep/api-dingus');
4
5 const { randomBytes } = require('node:crypto');
6 const { promisify } = require('node:util');
7 const randomBytesAsync = promisify(randomBytes);
8
9 /**
10 * Limit length of string to keep logs sane
11 * @param {string} str str
12 * @param {number} len len
13 * @returns {string} str
14 */
15 const logTruncate = (str, len) => {
16 if (typeof str !== 'string' || str.toString().length <= len) {
17 return str;
18 }
19 return str.toString().slice(0, len) + `... (${str.toString().length} bytes)`;
20 };
21
22 /**
23 * Turn a snake into a camel.
24 * @param {string} snakeCase snake case
25 * @param {string | RegExp} delimiter delimiter
26 * @returns {string} camel case
27 */
28 const camelfy = (snakeCase, delimiter = '_') => {
29 if (!snakeCase || typeof snakeCase.split !== 'function') {
30 return undefined;
31 }
32 const words = snakeCase.split(delimiter);
33 return [
34 words.shift(),
35 ...words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)),
36 ].join('');
37 };
38
39 /**
40 * Return an array containing x if x is not an array.
41 * @param {*} x x
42 * @returns {any[]} x[]
43 */
44 const ensureArray = (x) => {
45 if (x === undefined) {
46 return [];
47 }
48 if (!Array.isArray(x)) {
49 return Array(x);
50 }
51 return x;
52 };
53
54 /**
55 * Recursively freeze an object.
56 * @param {object} o obj
57 * @returns {object} frozen obj
58 */
59 const freezeDeep = (o) => {
60 Object.freeze(o);
61 Object.getOwnPropertyNames(o).forEach((prop) => {
62 if (Object.hasOwn(o, prop)
63 && ['object', 'function'].includes(typeof o[prop]) // eslint-disable-line security/detect-object-injection
64 && !Object.isFrozen(o[prop])) { // eslint-disable-line security/detect-object-injection
65 return freezeDeep(o[prop]); // eslint-disable-line security/detect-object-injection
66 }
67 });
68 return o;
69 };
70
71
72 /**
73 * Oauth2.1 §3.2.3.1
74 * %x20-21 / %x23-5B / %x5D-7E
75 * ' '-'!' / '#'-'[' / ']'-'~'
76 * not allowed: control characters, '"', '\'
77 * @param {string} char character
78 * @returns {boolean} is valid
79 */
80 const validErrorChar = (char) => {
81 const value = char.charCodeAt(0);
82 return value === 0x20 || value === 0x21
83 || (value >= 0x23 && value <= 0x5b)
84 || (value >= 0x5d && value <= 0x7e);
85 };
86
87
88 /**
89 * Determine if an OAuth error message is valid.
90 * @param {string} error error
91 * @returns {boolean} is valid
92 */
93 const validError = (error) => {
94 return error && error.split('').filter((c) => !validErrorChar(c)).length === 0 || false;
95 };
96
97
98 /**
99 * OAuth2.1 §3.2.2.1
100 * scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
101 * @param {string} char char
102 * @returns {boolean} is valid
103 */
104 const validScopeChar = (char) => {
105 const value = char.charCodeAt(0);
106 return value === 0x21
107 || (value >= 0x23 && value <= 0x5b)
108 || (value >= 0x5d && value <= 0x7e);
109 };
110
111
112 /**
113 * Determine if a scope has a valid name.
114 * @param {string} scope scope
115 * @returns {boolean} is valid
116 */
117 const validScope = (scope) => {
118 return scope && scope.split('').filter((c) => !validScopeChar(c)).length === 0 || false;
119 };
120
121
122 /**
123 *
124 * @param {number} bytes bytes
125 * @returns {string} base64 random string
126 */
127 const newSecret = async (bytes = 64) => {
128 return (await randomBytesAsync(bytes * 3 / 4)).toString('base64');
129 };
130
131
132 /**
133 * Convert a Date object to epoch seconds.
134 * @param {Date=} date date
135 * @returns {number} epoch
136 */
137 const dateToEpoch = (date) => {
138 const dateMs = date ? date.getTime() : Date.now();
139 return Math.ceil(dateMs / 1000);
140 };
141
142
143 const omit = (o, props) => {
144 return Object.fromEntries(Object.entries(o).filter(([k]) => !props.includes(k)));
145 };
146
147
148 /**
149 * @typedef {object} ConsoleLike
150 * @property {Function} debug log debug
151 */
152
153 /**
154 * Log Mystery Box statistics events.
155 * @param {ConsoleLike} logger logger instance
156 * @param {string} scope scope
157 * @returns {Function} stat logger
158 */
159 const mysteryBoxLogger = (logger, scope) => {
160 return (s) => {
161 logger.debug(scope, `${s.packageName}@${s.packageVersion}:${s.method}`, omit(s, [
162 'packageName',
163 'packageVersion',
164 'method',
165 ]));
166 };
167 };
168
169
170 const nop = () => { /**/ };
171
172 module.exports = {
173 ...common,
174 camelfy,
175 dateToEpoch,
176 ensureArray,
177 freezeDeep,
178 logTruncate,
179 mysteryBoxLogger,
180 newSecret,
181 omit,
182 randomBytesAsync,
183 validScope,
184 validError,
185 nop,
186 };
187