@anmiles/google-api-wrapper 7.0.1 → 7.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/lib/jsonLib.d.ts +1 -1
- package/dist/lib/jsonLib.js +2 -1
- package/dist/lib/jsonLib.js.map +1 -1
- package/dist/lib/secrets.js +37 -43
- package/dist/lib/secrets.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/__tests__/secrets.test.ts +136 -66
- package/src/lib/jsonLib.ts +1 -1
- package/src/lib/secrets.ts +44 -24
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [7.0.3](../../tags/v7.0.3) - 2023-04-22
|
|
9
|
+
### Changed
|
|
10
|
+
- Always require refresh token for permanent credentials
|
|
11
|
+
|
|
12
|
+
## [7.0.2](../../tags/v7.0.2) - 2023-04-22
|
|
13
|
+
### Changed
|
|
14
|
+
- Show instructions in the browser to prevent direct opening profile-oriented pages in wrong browsers
|
|
15
|
+
|
|
8
16
|
## [7.0.1](../../tags/v7.0.1) - 2023-04-22
|
|
9
17
|
### Changed
|
|
10
18
|
- Compatibility for `open` package
|
package/dist/lib/jsonLib.d.ts
CHANGED
package/dist/lib/jsonLib.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.writeJSON = exports.getJSONAsync = exports.getJSON = void 0;
|
|
6
|
+
exports.readJSON = exports.writeJSON = exports.getJSONAsync = exports.getJSON = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const logger_1 = require("./logger");
|
|
9
9
|
const paths_1 = require("./paths");
|
|
@@ -46,6 +46,7 @@ function readJSON(filename) {
|
|
|
46
46
|
const jsonString = fs_1.default.readFileSync(filename).toString();
|
|
47
47
|
return JSON.parse(jsonString);
|
|
48
48
|
}
|
|
49
|
+
exports.readJSON = readJSON;
|
|
49
50
|
function checkJSON(filename, json) {
|
|
50
51
|
if (json) {
|
|
51
52
|
return;
|
package/dist/lib/jsonLib.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonLib.js","sourceRoot":"","sources":["../../src/lib/jsonLib.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,qCAAiC;AACjC,mCAAqC;AAErC,wDAAgC;AAGhC,kBAAe,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAEzE,SAAS,OAAO,CAAI,QAAgB,EAAE,cAA8C,EAAE,YAAmC;IACxH,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE;YACxC,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjBQ,0BAAO;AAmBhB,KAAK,UAAU,YAAY,CAAI,QAAgB,EAAE,mBAAqC,EAAE,iBAAiD;IACxI,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,iBAAiB,IAAI,MAAM,iBAAiB,CAAC,IAAI,CAAC,EAAE;YACxD,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,MAAM,mBAAmB,EAAE,CAAC;IACzC,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjCiB,oCAAY;AAmC9B,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtD,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACxC,CAAC;AAtC+B,8BAAS;AAwCzC,SAAS,QAAQ,CAAI,QAAgB;IACpC,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxD,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAM,CAAC;AACpC,CAAC;
|
|
1
|
+
{"version":3,"file":"jsonLib.js","sourceRoot":"","sources":["../../src/lib/jsonLib.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,qCAAiC;AACjC,mCAAqC;AAErC,wDAAgC;AAGhC,kBAAe,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAEzE,SAAS,OAAO,CAAI,QAAgB,EAAE,cAA8C,EAAE,YAAmC;IACxH,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE;YACxC,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjBQ,0BAAO;AAmBhB,KAAK,UAAU,YAAY,CAAI,QAAgB,EAAE,mBAAqC,EAAE,iBAAiD;IACxI,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,iBAAiB,IAAI,MAAM,iBAAiB,CAAC,IAAI,CAAC,EAAE;YACxD,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,MAAM,mBAAmB,EAAE,CAAC;IACzC,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjCiB,oCAAY;AAmC9B,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtD,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACxC,CAAC;AAtC+B,8BAAS;AAwCzC,SAAS,QAAQ,CAAI,QAAgB;IACpC,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxD,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAM,CAAC;AACpC,CAAC;AA3C0C,4BAAQ;AA6CnD,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,IAAI,IAAI,EAAE;QACT,OAAO;KACP;IACD,IAAA,cAAK,EAAC,QAAQ,QAAQ,sGAAsG,CAAC,CAAC;AAC/H,CAAC"}
|
package/dist/lib/secrets.js
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
4
|
};
|
|
@@ -29,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
6
|
exports.getCredentials = exports.getSecrets = void 0;
|
|
30
7
|
const http_1 = __importDefault(require("http"));
|
|
31
8
|
const server_destroy_1 = __importDefault(require("server-destroy"));
|
|
32
|
-
const colorette = __importStar(require("colorette"));
|
|
33
9
|
const open_1 = __importDefault(require("open"));
|
|
34
10
|
const jsonLib_1 = require("./jsonLib");
|
|
35
11
|
const logger_1 = require("./logger");
|
|
@@ -37,6 +13,7 @@ const paths_1 = require("./paths");
|
|
|
37
13
|
const secrets_1 = __importDefault(require("./secrets"));
|
|
38
14
|
exports.default = { getScopes, getSecrets, getCredentials, validateCredentials, createCredentials, checkSecrets, getSecretsError, getScopesError };
|
|
39
15
|
const callbackPort = 6006;
|
|
16
|
+
const startURI = `http://localhost:${callbackPort}/`;
|
|
40
17
|
const callbackURI = `http://localhost:${callbackPort}/oauthcallback`;
|
|
41
18
|
const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
|
|
42
19
|
function getScopes() {
|
|
@@ -53,15 +30,25 @@ function getSecrets(profile) {
|
|
|
53
30
|
exports.getSecrets = getSecrets;
|
|
54
31
|
async function getCredentials(profile, auth, options) {
|
|
55
32
|
const credentialsFile = (0, paths_1.getCredentialsFile)(profile);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
33
|
+
if (options === null || options === void 0 ? void 0 : options.temporary) {
|
|
34
|
+
return secrets_1.default.createCredentials(profile, auth, options);
|
|
35
|
+
}
|
|
36
|
+
return (0, jsonLib_1.getJSONAsync)(credentialsFile, async () => {
|
|
37
|
+
// eslint-disable-next-line camelcase
|
|
38
|
+
const refresh_token = (0, paths_1.ensureFile)(credentialsFile) ? (0, jsonLib_1.readJSON)(credentialsFile).refresh_token : undefined;
|
|
39
|
+
const credentials = await secrets_1.default.createCredentials(profile, auth, options);
|
|
40
|
+
// eslint-disable-next-line camelcase
|
|
41
|
+
return { refresh_token, ...credentials };
|
|
42
|
+
}, secrets_1.default.validateCredentials);
|
|
59
43
|
}
|
|
60
44
|
exports.getCredentials = getCredentials;
|
|
61
45
|
async function validateCredentials(credentials) {
|
|
62
46
|
if (!credentials.access_token) {
|
|
63
47
|
return false;
|
|
64
48
|
}
|
|
49
|
+
if (!credentials.refresh_token) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
65
52
|
if (!credentials.expiry_date) {
|
|
66
53
|
return true;
|
|
67
54
|
}
|
|
@@ -73,31 +60,38 @@ async function createCredentials(profile, auth, options) {
|
|
|
73
60
|
const authUrl = auth.generateAuthUrl({
|
|
74
61
|
// eslint-disable-next-line camelcase
|
|
75
62
|
access_type: 'offline',
|
|
63
|
+
prompt: (options === null || options === void 0 ? void 0 : options.temporary) ? undefined : 'consent',
|
|
76
64
|
scope,
|
|
77
65
|
});
|
|
78
66
|
const server = http_1.default.createServer(async (request, response) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const code = url.searchParams.get('code');
|
|
83
|
-
if (!code) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
server.destroy();
|
|
87
|
-
const { tokens } = await auth.getToken(code);
|
|
88
|
-
resolve(tokens);
|
|
67
|
+
if (!request.url) {
|
|
68
|
+
response.end('');
|
|
69
|
+
return;
|
|
89
70
|
}
|
|
71
|
+
const url = new URL(`http://${request.headers.host}${request.url}`);
|
|
72
|
+
const code = url.searchParams.get('code');
|
|
73
|
+
if (!code) {
|
|
74
|
+
response.end(formatMessage(`Please open <a href="${authUrl}">this link</a> in a browser that belongs to <strong>${profile}</strong> google profile`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
response.end(formatMessage('Please close this page and return to application'));
|
|
78
|
+
server.destroy();
|
|
79
|
+
const { tokens } = await auth.getToken(code);
|
|
80
|
+
resolve(tokens);
|
|
90
81
|
});
|
|
91
82
|
(0, server_destroy_1.default)(server);
|
|
92
83
|
server.listen(callbackPort);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
(0, open_1.default)(authUrl);
|
|
98
|
-
}
|
|
84
|
+
(0, logger_1.warn)('Please check your browser for further actions');
|
|
85
|
+
(0, open_1.default)(startURI);
|
|
99
86
|
});
|
|
100
87
|
}
|
|
88
|
+
function formatMessage(message) {
|
|
89
|
+
return [
|
|
90
|
+
'<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">',
|
|
91
|
+
`<p>${message}</p>`,
|
|
92
|
+
'</div>',
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
101
95
|
function checkSecrets(profile, secretsObject, secretsFile) {
|
|
102
96
|
if (secretsObject.web.redirect_uris[0] === callbackURI) {
|
|
103
97
|
return true;
|
package/dist/lib/secrets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/lib/secrets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/lib/secrets.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import open from 'open';
|
|
|
5
5
|
import type GoogleApis from 'googleapis';
|
|
6
6
|
import jsonLib from '../jsonLib';
|
|
7
7
|
import logger from '../logger';
|
|
8
|
+
import paths from '../paths';
|
|
8
9
|
import type { Secrets } from '../../types';
|
|
9
10
|
|
|
10
11
|
import secrets from '../secrets';
|
|
@@ -14,7 +15,7 @@ jest.mock<typeof secrets>('../secrets', () => ({
|
|
|
14
15
|
getSecrets : jest.fn().mockImplementation(() => secretsJSON),
|
|
15
16
|
getCredentials : jest.fn(),
|
|
16
17
|
validateCredentials : jest.fn(),
|
|
17
|
-
createCredentials : jest.fn(),
|
|
18
|
+
createCredentials : jest.fn().mockImplementation(() => credentialsJSON),
|
|
18
19
|
checkSecrets : jest.fn(),
|
|
19
20
|
getScopesError : jest.fn().mockImplementation(() => scopesError),
|
|
20
21
|
getSecretsError : jest.fn().mockImplementation(() => secretsError),
|
|
@@ -41,20 +42,30 @@ jest.mock<Partial<typeof colorette>>('colorette', () => ({
|
|
|
41
42
|
yellow : jest.fn().mockImplementation((text) => `yellow:${text}`),
|
|
42
43
|
}));
|
|
43
44
|
|
|
44
|
-
jest.mock
|
|
45
|
+
jest.mock('open', () => jest.fn().mockImplementation((url: string) => {
|
|
46
|
+
willOpen(url.replace('http://localhost:6006', ''));
|
|
47
|
+
}));
|
|
45
48
|
|
|
46
49
|
jest.mock<Partial<typeof jsonLib>>('../jsonLib', () => ({
|
|
47
|
-
getJSON
|
|
50
|
+
getJSON : jest.fn().mockImplementation(() => json),
|
|
48
51
|
getJSONAsync : jest.fn().mockImplementation(async () => json),
|
|
52
|
+
readJSON : jest.fn().mockImplementation(async () => json),
|
|
49
53
|
}));
|
|
50
54
|
|
|
51
55
|
jest.mock<Partial<typeof logger>>('../logger', () => ({
|
|
52
|
-
|
|
56
|
+
warn : jest.fn(),
|
|
53
57
|
error : jest.fn().mockImplementation((error) => {
|
|
54
58
|
throw error;
|
|
55
59
|
}) as jest.Mock<never, any>,
|
|
56
60
|
}));
|
|
57
61
|
|
|
62
|
+
jest.mock<Partial<typeof paths>>('../paths', () => ({
|
|
63
|
+
getScopesFile : jest.fn().mockImplementation(() => scopesFile),
|
|
64
|
+
getSecretsFile : jest.fn().mockImplementation(() => secretsFile),
|
|
65
|
+
getCredentialsFile : jest.fn().mockImplementation(() => credentialsFile),
|
|
66
|
+
ensureFile : jest.fn().mockImplementation(() => fileExists),
|
|
67
|
+
}));
|
|
68
|
+
|
|
58
69
|
const profile = 'username1';
|
|
59
70
|
const scopesFile = 'scopes.json';
|
|
60
71
|
const secretsFile = 'secrets/username1.json';
|
|
@@ -83,11 +94,12 @@ const secretsJSON: Secrets = {
|
|
|
83
94
|
},
|
|
84
95
|
};
|
|
85
96
|
|
|
86
|
-
const credentialsJSON = {
|
|
87
|
-
|
|
97
|
+
const credentialsJSON: GoogleApis.Auth.Credentials = {
|
|
98
|
+
// eslint-disable-next-line camelcase
|
|
99
|
+
access_token : 'access_token222',
|
|
88
100
|
};
|
|
89
101
|
|
|
90
|
-
let json:
|
|
102
|
+
let json: any;
|
|
91
103
|
|
|
92
104
|
const code = 'code';
|
|
93
105
|
const authUrl = 'https://authUrl';
|
|
@@ -96,8 +108,6 @@ const auth = {
|
|
|
96
108
|
getToken : jest.fn().mockResolvedValue({ tokens : credentialsJSON }),
|
|
97
109
|
} as unknown as GoogleApis.Common.OAuth2Client;
|
|
98
110
|
|
|
99
|
-
let request: http.IncomingMessage;
|
|
100
|
-
|
|
101
111
|
const response = {
|
|
102
112
|
end : jest.fn(),
|
|
103
113
|
} as unknown as http.ServerResponse;
|
|
@@ -107,6 +117,16 @@ let serverCallback: (
|
|
|
107
117
|
response: http.ServerResponse
|
|
108
118
|
) => Promise<typeof credentialsJSON>;
|
|
109
119
|
|
|
120
|
+
function willOpen(url: string | undefined, timeout?: number) {
|
|
121
|
+
setTimeout(async () => {
|
|
122
|
+
await serverCallback({
|
|
123
|
+
url,
|
|
124
|
+
headers : {
|
|
125
|
+
host : 'localhost:6006',
|
|
126
|
+
},
|
|
127
|
+
} as http.IncomingMessage, response);
|
|
128
|
+
}, timeout || 0);
|
|
129
|
+
}
|
|
110
130
|
let closedTime: number;
|
|
111
131
|
|
|
112
132
|
const on = jest.fn().mockImplementation((event: string, listener: (...args: any[]) => void) => {
|
|
@@ -128,6 +148,8 @@ const connections = [
|
|
|
128
148
|
{ remoteAddress : 'server', remotePort : '1003', on : jest.fn(), destroy : jest.fn() },
|
|
129
149
|
];
|
|
130
150
|
|
|
151
|
+
let fileExists: boolean;
|
|
152
|
+
|
|
131
153
|
describe('src/lib/secrets', () => {
|
|
132
154
|
describe('getScopes', () => {
|
|
133
155
|
const getJSONSpy = jest.spyOn(jsonLib, 'getJSON');
|
|
@@ -193,7 +215,8 @@ describe('src/lib/secrets', () => {
|
|
|
193
215
|
const getJSONAsyncSpy = jest.spyOn(jsonLib, 'getJSONAsync');
|
|
194
216
|
|
|
195
217
|
beforeEach(() => {
|
|
196
|
-
json
|
|
218
|
+
json = credentialsJSON;
|
|
219
|
+
fileExists = false;
|
|
197
220
|
});
|
|
198
221
|
|
|
199
222
|
it('should get json from credentials file by default', async () => {
|
|
@@ -203,7 +226,7 @@ describe('src/lib/secrets', () => {
|
|
|
203
226
|
expect(getJSONAsyncSpy.mock.calls[0][0]).toEqual(credentialsFile);
|
|
204
227
|
});
|
|
205
228
|
|
|
206
|
-
it('should get json from credentials file if temporariness
|
|
229
|
+
it('should get json from credentials file if temporariness not set', async () => {
|
|
207
230
|
await original.getCredentials(profile, auth, { temporary : false });
|
|
208
231
|
|
|
209
232
|
expect(getJSONAsyncSpy).toBeCalled();
|
|
@@ -216,18 +239,22 @@ describe('src/lib/secrets', () => {
|
|
|
216
239
|
expect(getJSONAsyncSpy).not.toBeCalled();
|
|
217
240
|
});
|
|
218
241
|
|
|
219
|
-
it('should
|
|
242
|
+
it('should call createCredentials in fallback', async () => {
|
|
220
243
|
await original.getCredentials(profile, auth);
|
|
221
244
|
|
|
245
|
+
expect(secrets.createCredentials).not.toBeCalled();
|
|
246
|
+
|
|
222
247
|
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
223
248
|
await fallback();
|
|
224
249
|
|
|
225
250
|
expect(secrets.createCredentials).toBeCalledWith(profile, auth, undefined);
|
|
226
251
|
});
|
|
227
252
|
|
|
228
|
-
it('should call createCredentials
|
|
253
|
+
it('should call createCredentials in fallback if temporariness not set', async () => {
|
|
229
254
|
await original.getCredentials(profile, auth, { temporary : false });
|
|
230
255
|
|
|
256
|
+
expect(secrets.createCredentials).not.toBeCalled();
|
|
257
|
+
|
|
231
258
|
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
232
259
|
await fallback();
|
|
233
260
|
|
|
@@ -240,22 +267,60 @@ describe('src/lib/secrets', () => {
|
|
|
240
267
|
expect(secrets.createCredentials).toBeCalledWith(profile, auth, { temporary : true });
|
|
241
268
|
});
|
|
242
269
|
|
|
243
|
-
it('should return credentials
|
|
244
|
-
|
|
270
|
+
it('should return credentials in fallback', async () => {
|
|
271
|
+
await original.getCredentials(profile, auth);
|
|
272
|
+
|
|
273
|
+
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
274
|
+
const result = await fallback();
|
|
245
275
|
|
|
246
276
|
expect(result).toEqual(credentialsJSON);
|
|
247
277
|
});
|
|
248
278
|
|
|
249
|
-
it('should return credentials if
|
|
250
|
-
|
|
279
|
+
it('should return credentials in fallback and copy existing refresh token from existing file if created credentials do not have refresh token', async () => {
|
|
280
|
+
fileExists = true;
|
|
281
|
+
// eslint-disable-next-line camelcase
|
|
282
|
+
jest.spyOn(jsonLib, 'readJSON').mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token' });
|
|
251
283
|
|
|
284
|
+
await original.getCredentials(profile, auth);
|
|
285
|
+
|
|
286
|
+
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
287
|
+
const result = await fallback();
|
|
288
|
+
|
|
289
|
+
expect(jsonLib.readJSON).toBeCalledWith(credentialsFile);
|
|
290
|
+
// eslint-disable-next-line camelcase
|
|
291
|
+
expect(result).toEqual({ ...credentialsJSON, refresh_token : 'refresh_token' });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should return credentials in fallback and leave refresh_token undefined if there is no existing file', async () => {
|
|
295
|
+
fileExists = false;
|
|
296
|
+
// eslint-disable-next-line camelcase
|
|
297
|
+
jest.spyOn(jsonLib, 'readJSON').mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token' });
|
|
298
|
+
|
|
299
|
+
await original.getCredentials(profile, auth);
|
|
300
|
+
|
|
301
|
+
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
302
|
+
const result = await fallback();
|
|
303
|
+
|
|
304
|
+
expect(jsonLib.readJSON).not.toBeCalled();
|
|
305
|
+
// eslint-disable-next-line camelcase
|
|
252
306
|
expect(result).toEqual(credentialsJSON);
|
|
253
307
|
});
|
|
254
308
|
|
|
255
|
-
it('should return
|
|
256
|
-
|
|
309
|
+
it('should return credentials in fallback and leave refresh_token as is if it is set from createCredentials', async () => {
|
|
310
|
+
fileExists = true;
|
|
311
|
+
// eslint-disable-next-line camelcase
|
|
312
|
+
jest.spyOn(secrets, 'createCredentials').mockResolvedValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token_1' });
|
|
313
|
+
// eslint-disable-next-line camelcase
|
|
314
|
+
jest.spyOn(jsonLib, 'readJSON').mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token_2' });
|
|
315
|
+
|
|
316
|
+
await original.getCredentials(profile, auth);
|
|
317
|
+
|
|
318
|
+
const fallback = getJSONAsyncSpy.mock.calls[0][1];
|
|
319
|
+
const result = await fallback();
|
|
257
320
|
|
|
258
|
-
expect(
|
|
321
|
+
expect(jsonLib.readJSON).toBeCalledWith(credentialsFile);
|
|
322
|
+
// eslint-disable-next-line camelcase
|
|
323
|
+
expect(result).toEqual({ ...credentialsJSON, refresh_token : 'refresh_token_1' });
|
|
259
324
|
});
|
|
260
325
|
});
|
|
261
326
|
|
|
@@ -264,47 +329,55 @@ describe('src/lib/secrets', () => {
|
|
|
264
329
|
expect(await original.validateCredentials({})).toEqual(false);
|
|
265
330
|
});
|
|
266
331
|
|
|
332
|
+
it('should return true if no refresh token', async () => {
|
|
333
|
+
// eslint-disable-next-line camelcase
|
|
334
|
+
expect(await original.validateCredentials({ access_token : 'token' })).toEqual(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
267
337
|
it('should return true if no expiration', async () => {
|
|
268
338
|
// eslint-disable-next-line camelcase
|
|
269
|
-
expect(await original.validateCredentials({ access_token : 'token' })).toEqual(true);
|
|
339
|
+
expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token' })).toEqual(true);
|
|
270
340
|
});
|
|
271
341
|
|
|
272
342
|
it('should return true if credentials are not more than 1 week ago', async () => {
|
|
273
343
|
const expiryDate = new Date();
|
|
274
344
|
expiryDate.setDate(expiryDate.getDate() - 6);
|
|
275
345
|
// eslint-disable-next-line camelcase
|
|
276
|
-
expect(await original.validateCredentials({ access_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(true);
|
|
346
|
+
expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(true);
|
|
277
347
|
});
|
|
278
348
|
|
|
279
349
|
it('should return true if credentials are more than 1 week ago', async () => {
|
|
280
350
|
const expiryDate = new Date();
|
|
281
351
|
expiryDate.setDate(expiryDate.getDate() - 8);
|
|
282
352
|
// eslint-disable-next-line camelcase
|
|
283
|
-
expect(await original.validateCredentials({ access_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(false);
|
|
353
|
+
expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(false);
|
|
284
354
|
});
|
|
285
355
|
});
|
|
286
356
|
|
|
287
357
|
describe('createCredentials', () => {
|
|
288
|
-
|
|
289
|
-
setTimeout(async () => {
|
|
290
|
-
await serverCallback(request, response);
|
|
291
|
-
}, timeout);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
beforeEach(() => {
|
|
295
|
-
request = {
|
|
296
|
-
url : `/request.url?code=${code}`,
|
|
297
|
-
headers : {
|
|
298
|
-
host : 'localhost:6006',
|
|
299
|
-
},
|
|
300
|
-
} as http.IncomingMessage;
|
|
301
|
-
});
|
|
358
|
+
const tokenUrl = `/request.url?code=${code}`;
|
|
302
359
|
|
|
303
360
|
it('should generate authUrl', async () => {
|
|
304
|
-
willOpen(
|
|
361
|
+
willOpen(tokenUrl, 100);
|
|
305
362
|
|
|
306
363
|
await original.createCredentials(profile, auth);
|
|
307
364
|
|
|
365
|
+
expect(auth.generateAuthUrl).toBeCalledWith({
|
|
366
|
+
// eslint-disable-next-line camelcase
|
|
367
|
+
access_type : 'offline',
|
|
368
|
+
prompt : 'consent',
|
|
369
|
+
scope : [
|
|
370
|
+
'https://www.googleapis.com/auth/calendar.calendars.readonly',
|
|
371
|
+
'https://www.googleapis.com/auth/calendar.events.readonly',
|
|
372
|
+
],
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should generate authUrl and do not require consent if credentials are temporary because refresh token is not required in credentials JSON', async () => {
|
|
377
|
+
willOpen(tokenUrl, 100);
|
|
378
|
+
|
|
379
|
+
await original.createCredentials(profile, auth, { temporary : true });
|
|
380
|
+
|
|
308
381
|
expect(auth.generateAuthUrl).toBeCalledWith({
|
|
309
382
|
// eslint-disable-next-line camelcase
|
|
310
383
|
access_type : 'offline',
|
|
@@ -316,19 +389,20 @@ describe('src/lib/secrets', () => {
|
|
|
316
389
|
});
|
|
317
390
|
|
|
318
391
|
it('should generate authUrl with custom scopes', async () => {
|
|
319
|
-
willOpen(
|
|
392
|
+
willOpen(tokenUrl, 100);
|
|
320
393
|
|
|
321
394
|
await original.createCredentials(profile, auth, { scopes : [ 'scope1', 'scope2' ] });
|
|
322
395
|
|
|
323
396
|
expect(auth.generateAuthUrl).toBeCalledWith({
|
|
324
397
|
// eslint-disable-next-line camelcase
|
|
325
398
|
access_type : 'offline',
|
|
399
|
+
prompt : 'consent',
|
|
326
400
|
scope : [ 'scope1', 'scope2' ],
|
|
327
401
|
});
|
|
328
402
|
});
|
|
329
403
|
|
|
330
404
|
it('should create server on 6006 port', async () => {
|
|
331
|
-
willOpen(
|
|
405
|
+
willOpen(tokenUrl, 100);
|
|
332
406
|
|
|
333
407
|
await original.createCredentials(profile, auth);
|
|
334
408
|
|
|
@@ -336,43 +410,43 @@ describe('src/lib/secrets', () => {
|
|
|
336
410
|
expect(listen).toBeCalledWith(6006);
|
|
337
411
|
});
|
|
338
412
|
|
|
339
|
-
it('should open browser page
|
|
340
|
-
willOpen(
|
|
413
|
+
it('should open browser page and warn about it', async () => {
|
|
414
|
+
willOpen(tokenUrl, 100);
|
|
341
415
|
|
|
342
416
|
await original.createCredentials(profile, auth);
|
|
343
417
|
|
|
344
|
-
expect(open).toBeCalledWith('
|
|
345
|
-
expect(logger.
|
|
418
|
+
expect(open).toBeCalledWith('http://localhost:6006/');
|
|
419
|
+
expect(logger.warn).toBeCalledWith('Please check your browser for further actions');
|
|
346
420
|
});
|
|
347
421
|
|
|
348
|
-
it('should
|
|
349
|
-
willOpen(
|
|
422
|
+
it('should show nothing on the browser page if request.url is empty', async () => {
|
|
423
|
+
willOpen('', 100);
|
|
424
|
+
willOpen(tokenUrl, 200);
|
|
350
425
|
|
|
351
|
-
await original.createCredentials(profile, auth
|
|
426
|
+
await original.createCredentials(profile, auth);
|
|
352
427
|
|
|
353
|
-
expect(
|
|
354
|
-
expect(logger.info).toBeCalledWith(`Please open yellow:https://authUrl in your browser using google profile for yellow:${profile} and allow access to yellow:https://www.googleapis.com/auth/calendar.calendars.readonly,https://www.googleapis.com/auth/calendar.events.readonly`);
|
|
428
|
+
expect(response.end).toBeCalledWith('');
|
|
355
429
|
});
|
|
356
430
|
|
|
357
|
-
it('should
|
|
358
|
-
willOpen(
|
|
431
|
+
it('should show opening instructions if opened the home page', async () => {
|
|
432
|
+
willOpen('/', 100);
|
|
433
|
+
willOpen(tokenUrl, 200);
|
|
359
434
|
|
|
360
|
-
await original.createCredentials(profile, auth
|
|
435
|
+
await original.createCredentials(profile, auth);
|
|
361
436
|
|
|
362
|
-
expect(
|
|
363
|
-
expect(logger.info).toBeCalledWith(`Please open yellow:https://authUrl in your browser using google profile for yellow:${profile} and allow access to yellow:scope1,scope2`);
|
|
437
|
+
expect(response.end).toBeCalledWith(`<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">\n<p>Please open <a href="${authUrl}">this link</a> in a browser that belongs to <strong>${profile}</strong> google profile</p>\n</div>`);
|
|
364
438
|
});
|
|
365
439
|
|
|
366
440
|
it('should ask to close webpage', async () => {
|
|
367
|
-
willOpen(
|
|
441
|
+
willOpen(tokenUrl, 100);
|
|
368
442
|
|
|
369
443
|
await original.createCredentials(profile, auth);
|
|
370
444
|
|
|
371
|
-
expect(response.end).toBeCalledWith('<
|
|
445
|
+
expect(response.end).toBeCalledWith('<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">\n<p>Please close this page and return to application</p>\n</div>');
|
|
372
446
|
});
|
|
373
447
|
|
|
374
448
|
it('should close server and destroy all connections if request.url is truthy', async () => {
|
|
375
|
-
willOpen(
|
|
449
|
+
willOpen(tokenUrl, 100);
|
|
376
450
|
|
|
377
451
|
await original.createCredentials(profile, auth);
|
|
378
452
|
|
|
@@ -384,12 +458,10 @@ describe('src/lib/secrets', () => {
|
|
|
384
458
|
it('should only resolve when request.url is truthy', async () => {
|
|
385
459
|
const emptyRequestTime = 100;
|
|
386
460
|
const requestTime = 200;
|
|
387
|
-
const emptyRequest = { ...request } as http.IncomingMessage;
|
|
388
|
-
emptyRequest.url = undefined;
|
|
389
461
|
|
|
390
462
|
const before = new Date().getTime();
|
|
391
|
-
willOpen(
|
|
392
|
-
willOpen(
|
|
463
|
+
willOpen(undefined, emptyRequestTime);
|
|
464
|
+
willOpen(tokenUrl, requestTime);
|
|
393
465
|
|
|
394
466
|
const result = await original.createCredentials(profile, auth);
|
|
395
467
|
const after = new Date().getTime();
|
|
@@ -403,12 +475,10 @@ describe('src/lib/secrets', () => {
|
|
|
403
475
|
it('should only resolve when request.url contains no code', async () => {
|
|
404
476
|
const noCodeRequestTime = 100;
|
|
405
477
|
const requestTime = 200;
|
|
406
|
-
const noCodeRequest = { ...request } as http.IncomingMessage;
|
|
407
|
-
noCodeRequest.url = '/request.url?param=value';
|
|
408
478
|
|
|
409
479
|
const before = new Date().getTime();
|
|
410
|
-
willOpen(
|
|
411
|
-
willOpen(
|
|
480
|
+
willOpen('/request.url?param=value', noCodeRequestTime);
|
|
481
|
+
willOpen(tokenUrl, requestTime);
|
|
412
482
|
|
|
413
483
|
const result = await original.createCredentials(profile, auth);
|
|
414
484
|
const after = new Date().getTime();
|
|
@@ -420,7 +490,7 @@ describe('src/lib/secrets', () => {
|
|
|
420
490
|
});
|
|
421
491
|
|
|
422
492
|
it('should return credentials JSON', async () => {
|
|
423
|
-
willOpen(
|
|
493
|
+
willOpen(tokenUrl, 100);
|
|
424
494
|
|
|
425
495
|
const result = await original.createCredentials(profile, auth);
|
|
426
496
|
|
package/src/lib/jsonLib.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ensureFile } from './paths';
|
|
|
4
4
|
|
|
5
5
|
import jsonLib from './jsonLib';
|
|
6
6
|
|
|
7
|
-
export { getJSON, getJSONAsync, writeJSON };
|
|
7
|
+
export { getJSON, getJSONAsync, writeJSON, readJSON };
|
|
8
8
|
export default { getJSON, getJSONAsync, writeJSON, readJSON, checkJSON };
|
|
9
9
|
|
|
10
10
|
function getJSON<T>(filename: string, createCallback: () => Exclude<T, Promise<any>>, validateJSON?: (json: T) => boolean): T {
|
package/src/lib/secrets.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import enableDestroy from 'server-destroy';
|
|
3
|
-
import * as colorette from 'colorette';
|
|
4
3
|
import open from 'open';
|
|
5
4
|
import type GoogleApis from 'googleapis';
|
|
6
5
|
import type { Secrets, AuthOptions } from '../types';
|
|
7
|
-
import { getJSON, getJSONAsync } from './jsonLib';
|
|
8
|
-
import {
|
|
9
|
-
import { getScopesFile, getSecretsFile, getCredentialsFile } from './paths';
|
|
6
|
+
import { getJSON, getJSONAsync, readJSON } from './jsonLib';
|
|
7
|
+
import { warn, error } from './logger';
|
|
8
|
+
import { getScopesFile, getSecretsFile, getCredentialsFile, ensureFile } from './paths';
|
|
10
9
|
|
|
11
10
|
import secrets from './secrets';
|
|
12
11
|
|
|
@@ -14,6 +13,7 @@ export { getSecrets, getCredentials };
|
|
|
14
13
|
export default { getScopes, getSecrets, getCredentials, validateCredentials, createCredentials, checkSecrets, getSecretsError, getScopesError };
|
|
15
14
|
|
|
16
15
|
const callbackPort = 6006;
|
|
16
|
+
const startURI = `http://localhost:${callbackPort}/`;
|
|
17
17
|
const callbackURI = `http://localhost:${callbackPort}/oauthcallback`;
|
|
18
18
|
const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
|
|
19
19
|
|
|
@@ -33,9 +33,17 @@ function getSecrets(profile: string): Secrets {
|
|
|
33
33
|
async function getCredentials(profile: string, auth: GoogleApis.Common.OAuth2Client, options?: AuthOptions): Promise<GoogleApis.Auth.Credentials> {
|
|
34
34
|
const credentialsFile = getCredentialsFile(profile);
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (options?.temporary) {
|
|
37
|
+
return secrets.createCredentials(profile, auth, options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return getJSONAsync(credentialsFile, async () => {
|
|
41
|
+
// eslint-disable-next-line camelcase
|
|
42
|
+
const refresh_token = ensureFile(credentialsFile) ? readJSON<GoogleApis.Auth.Credentials>(credentialsFile).refresh_token : undefined;
|
|
43
|
+
const credentials = await secrets.createCredentials(profile, auth, options);
|
|
44
|
+
// eslint-disable-next-line camelcase
|
|
45
|
+
return { refresh_token, ...credentials };
|
|
46
|
+
}, secrets.validateCredentials);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Promise<boolean> {
|
|
@@ -43,6 +51,10 @@ async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Pr
|
|
|
43
51
|
return false;
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
if (!credentials.refresh_token) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
if (!credentials.expiry_date) {
|
|
47
59
|
return true;
|
|
48
60
|
}
|
|
@@ -57,37 +69,45 @@ async function createCredentials(profile: string, auth: GoogleApis.Auth.OAuth2Cl
|
|
|
57
69
|
const authUrl = auth.generateAuthUrl({
|
|
58
70
|
// eslint-disable-next-line camelcase
|
|
59
71
|
access_type : 'offline',
|
|
72
|
+
prompt : options?.temporary ? undefined : 'consent',
|
|
60
73
|
scope,
|
|
61
74
|
});
|
|
62
75
|
|
|
63
76
|
const server = http.createServer(async (request, response) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const code = url.searchParams.get('code');
|
|
77
|
+
if (!request.url) {
|
|
78
|
+
response.end('');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
82
|
+
const url = new URL(`http://${request.headers.host}${request.url}`);
|
|
83
|
+
const code = url.searchParams.get('code');
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
if (!code) {
|
|
86
|
+
response.end(formatMessage(`Please open <a href="${authUrl}">this link</a> in a browser that belongs to <strong>${profile}</strong> google profile`));
|
|
87
|
+
return;
|
|
77
88
|
}
|
|
89
|
+
|
|
90
|
+
response.end(formatMessage('Please close this page and return to application'));
|
|
91
|
+
server.destroy();
|
|
92
|
+
const { tokens } = await auth.getToken(code);
|
|
93
|
+
resolve(tokens);
|
|
78
94
|
});
|
|
79
95
|
|
|
80
96
|
enableDestroy(server);
|
|
81
97
|
server.listen(callbackPort);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
info(`Please open ${colorette.yellow(authUrl)} in your browser using google profile for ${colorette.yellow(profile)} and allow access to ${colorette.yellow(scope.join(','))}`);
|
|
85
|
-
} else {
|
|
86
|
-
open(authUrl);
|
|
87
|
-
}
|
|
98
|
+
warn('Please check your browser for further actions');
|
|
99
|
+
open(startURI);
|
|
88
100
|
});
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
function formatMessage(message: string): string {
|
|
104
|
+
return [
|
|
105
|
+
'<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">',
|
|
106
|
+
`<p>${message}</p>`,
|
|
107
|
+
'</div>',
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
function checkSecrets(profile: string, secretsObject: Secrets, secretsFile: string): true | void {
|
|
92
112
|
if (secretsObject.web.redirect_uris[0] === callbackURI) {
|
|
93
113
|
return true;
|