nworks 1.2.1 → 1.2.2
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/LICENSE +190 -190
- package/README.ja.md +393 -393
- package/README.ko.md +393 -393
- package/README.md +393 -393
- package/dist/index.js +286 -85
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +269 -80
- package/dist/mcp.js.map +1 -1
- package/package.json +58 -58
package/dist/mcp.js
CHANGED
|
@@ -31,13 +31,14 @@ import { join } from "path";
|
|
|
31
31
|
function hasServiceAccountCreds(creds) {
|
|
32
32
|
return !!(creds.serviceAccount && creds.privateKeyPath && creds.botId);
|
|
33
33
|
}
|
|
34
|
+
var IS_UNIX = process.platform !== "win32";
|
|
34
35
|
var CONFIG_DIR = join(homedir(), ".config", "nworks");
|
|
35
36
|
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
36
37
|
var TOKEN_PATH = join(CONFIG_DIR, "token.json");
|
|
37
38
|
var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
|
|
38
39
|
async function ensureConfigDir() {
|
|
39
40
|
if (!existsSync(CONFIG_DIR)) {
|
|
40
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
41
|
+
await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
function getCredentialsFromEnv() {
|
|
@@ -77,7 +78,11 @@ async function saveCredentials(creds, profile = "default") {
|
|
|
77
78
|
profiles = JSON.parse(raw);
|
|
78
79
|
}
|
|
79
80
|
profiles[profile] = creds;
|
|
80
|
-
await writeFile(
|
|
81
|
+
await writeFile(
|
|
82
|
+
CREDENTIALS_PATH,
|
|
83
|
+
JSON.stringify(profiles, null, 2),
|
|
84
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
85
|
+
);
|
|
81
86
|
}
|
|
82
87
|
async function loadToken(profile = "default") {
|
|
83
88
|
if (!existsSync(TOKEN_PATH)) return null;
|
|
@@ -98,7 +103,11 @@ async function saveToken(token, profile = "default") {
|
|
|
98
103
|
tokens = JSON.parse(raw);
|
|
99
104
|
}
|
|
100
105
|
tokens[profile] = token;
|
|
101
|
-
await writeFile(
|
|
106
|
+
await writeFile(
|
|
107
|
+
TOKEN_PATH,
|
|
108
|
+
JSON.stringify(tokens, null, 2),
|
|
109
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
110
|
+
);
|
|
102
111
|
}
|
|
103
112
|
async function loadUserToken(profile = "default") {
|
|
104
113
|
if (!existsSync(USER_TOKEN_PATH)) return null;
|
|
@@ -121,7 +130,11 @@ async function saveUserToken(token, profile = "default") {
|
|
|
121
130
|
tokens = JSON.parse(raw);
|
|
122
131
|
}
|
|
123
132
|
tokens[profile] = token;
|
|
124
|
-
await writeFile(
|
|
133
|
+
await writeFile(
|
|
134
|
+
USER_TOKEN_PATH,
|
|
135
|
+
JSON.stringify(tokens, null, 2),
|
|
136
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
137
|
+
);
|
|
125
138
|
}
|
|
126
139
|
async function clearCredentials(profile = "default") {
|
|
127
140
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
@@ -131,25 +144,33 @@ async function clearCredentials(profile = "default") {
|
|
|
131
144
|
await writeFile(
|
|
132
145
|
CREDENTIALS_PATH,
|
|
133
146
|
JSON.stringify(profiles, null, 2),
|
|
134
|
-
"utf-8"
|
|
147
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
135
148
|
);
|
|
136
149
|
}
|
|
137
150
|
if (existsSync(TOKEN_PATH)) {
|
|
138
151
|
const raw = await readFile(TOKEN_PATH, "utf-8");
|
|
139
152
|
const tokens = JSON.parse(raw);
|
|
140
153
|
delete tokens[profile];
|
|
141
|
-
await writeFile(
|
|
154
|
+
await writeFile(
|
|
155
|
+
TOKEN_PATH,
|
|
156
|
+
JSON.stringify(tokens, null, 2),
|
|
157
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
158
|
+
);
|
|
142
159
|
}
|
|
143
160
|
if (existsSync(USER_TOKEN_PATH)) {
|
|
144
161
|
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
145
162
|
const tokens = JSON.parse(raw);
|
|
146
163
|
delete tokens[profile];
|
|
147
|
-
await writeFile(
|
|
164
|
+
await writeFile(
|
|
165
|
+
USER_TOKEN_PATH,
|
|
166
|
+
JSON.stringify(tokens, null, 2),
|
|
167
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
168
|
+
);
|
|
148
169
|
}
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
// src/auth/jwt.ts
|
|
152
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
173
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
153
174
|
import jwt from "jsonwebtoken";
|
|
154
175
|
async function createJWT(creds) {
|
|
155
176
|
if (!creds.serviceAccount || !creds.privateKeyPath) {
|
|
@@ -158,6 +179,15 @@ async function createJWT(creds) {
|
|
|
158
179
|
);
|
|
159
180
|
}
|
|
160
181
|
const privateKey = await readFile2(creds.privateKeyPath, "utf-8");
|
|
182
|
+
if (process.platform !== "win32") {
|
|
183
|
+
const fileStat = await stat(creds.privateKeyPath);
|
|
184
|
+
const mode = fileStat.mode & 511;
|
|
185
|
+
if (mode & 63) {
|
|
186
|
+
console.error(
|
|
187
|
+
`[nworks] Warning: Private key file has permissions ${mode.toString(8)}. Recommended: 600`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
161
191
|
const now = Math.floor(Date.now() / 1e3);
|
|
162
192
|
const payload = {
|
|
163
193
|
iss: creds.clientId,
|
|
@@ -194,7 +224,8 @@ async function refreshToken(profile = "default") {
|
|
|
194
224
|
});
|
|
195
225
|
if (!res.ok) {
|
|
196
226
|
const text = await res.text();
|
|
197
|
-
|
|
227
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
228
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
|
|
198
229
|
}
|
|
199
230
|
const data = await res.json();
|
|
200
231
|
const expiresIn = Number(data.expires_in);
|
|
@@ -210,7 +241,7 @@ async function refreshToken(profile = "default") {
|
|
|
210
241
|
var BASE_URL = "https://www.worksapis.com/v1.0";
|
|
211
242
|
var MAX_RETRIES = 3;
|
|
212
243
|
function sleep(ms) {
|
|
213
|
-
return new Promise((
|
|
244
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
214
245
|
}
|
|
215
246
|
async function request(opts, _retryCount = 0) {
|
|
216
247
|
const { method, path, body, profile = "default" } = opts;
|
|
@@ -232,7 +263,9 @@ async function request(opts, _retryCount = 0) {
|
|
|
232
263
|
return request(opts, _retryCount + 1);
|
|
233
264
|
}
|
|
234
265
|
if (res.status === 429 && _retryCount < MAX_RETRIES) {
|
|
235
|
-
const
|
|
266
|
+
const MAX_RETRY_AFTER = 60;
|
|
267
|
+
const rawRetry = parseInt(res.headers.get("Retry-After") ?? "5", 10);
|
|
268
|
+
const retryAfter = Math.min(Number.isNaN(rawRetry) ? 5 : rawRetry, MAX_RETRY_AFTER);
|
|
236
269
|
await sleep(retryAfter * 1e3);
|
|
237
270
|
return request(opts, _retryCount + 1);
|
|
238
271
|
}
|
|
@@ -257,6 +290,58 @@ async function request(opts, _retryCount = 0) {
|
|
|
257
290
|
return JSON.parse(text);
|
|
258
291
|
}
|
|
259
292
|
|
|
293
|
+
// src/utils/sanitize.ts
|
|
294
|
+
import { basename, resolve, sep } from "path";
|
|
295
|
+
import { randomBytes } from "crypto";
|
|
296
|
+
function sanitizePathSegment(value) {
|
|
297
|
+
if (!value || typeof value !== "string") {
|
|
298
|
+
throw new Error("Path segment must be a non-empty string");
|
|
299
|
+
}
|
|
300
|
+
if (value === "me") return value;
|
|
301
|
+
if (/[/\\]/.test(value) || value.includes("..")) {
|
|
302
|
+
throw new Error(`Invalid path segment: "${value}"`);
|
|
303
|
+
}
|
|
304
|
+
return encodeURIComponent(value);
|
|
305
|
+
}
|
|
306
|
+
function sanitizeFileName(name) {
|
|
307
|
+
if (!name || typeof name !== "string") {
|
|
308
|
+
throw new Error("File name must be a non-empty string");
|
|
309
|
+
}
|
|
310
|
+
const base = basename(name);
|
|
311
|
+
return base.replace(/[\r\n"\\]/g, "_");
|
|
312
|
+
}
|
|
313
|
+
function validateLocalPath(filePath, allowedBase) {
|
|
314
|
+
const resolved = resolve(filePath);
|
|
315
|
+
if (allowedBase) {
|
|
316
|
+
const resolvedBase = resolve(allowedBase);
|
|
317
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
|
|
318
|
+
throw new Error(`Path "${filePath}" escapes the allowed directory "${allowedBase}"`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return resolved;
|
|
322
|
+
}
|
|
323
|
+
function validateRedirectUrl(location, allowedHosts) {
|
|
324
|
+
let parsed;
|
|
325
|
+
try {
|
|
326
|
+
parsed = new URL(location);
|
|
327
|
+
} catch {
|
|
328
|
+
throw new Error(`Invalid redirect URL: "${location}"`);
|
|
329
|
+
}
|
|
330
|
+
if (parsed.protocol !== "https:") {
|
|
331
|
+
throw new Error(`Redirect URL must use HTTPS: "${location}"`);
|
|
332
|
+
}
|
|
333
|
+
const isAllowed = allowedHosts.some(
|
|
334
|
+
(host) => parsed.hostname === host || parsed.hostname.endsWith("." + host)
|
|
335
|
+
);
|
|
336
|
+
if (!isAllowed) {
|
|
337
|
+
throw new Error(`Redirect to untrusted host: "${parsed.hostname}"`);
|
|
338
|
+
}
|
|
339
|
+
return location;
|
|
340
|
+
}
|
|
341
|
+
function generateSecureState() {
|
|
342
|
+
return randomBytes(32).toString("hex");
|
|
343
|
+
}
|
|
344
|
+
|
|
260
345
|
// src/api/message.ts
|
|
261
346
|
function buildContent(opts) {
|
|
262
347
|
const type = opts.type ?? "text";
|
|
@@ -264,7 +349,11 @@ function buildContent(opts) {
|
|
|
264
349
|
return { type: "text", text: opts.text };
|
|
265
350
|
}
|
|
266
351
|
if (type === "button") {
|
|
267
|
-
const actions = opts.actions ?
|
|
352
|
+
const actions = opts.actions ? (() => {
|
|
353
|
+
const parsed = JSON.parse(opts.actions);
|
|
354
|
+
if (!Array.isArray(parsed)) throw new Error("actions must be a JSON array");
|
|
355
|
+
return parsed;
|
|
356
|
+
})() : [];
|
|
268
357
|
return {
|
|
269
358
|
type: "button_template",
|
|
270
359
|
contentText: opts.text,
|
|
@@ -272,7 +361,11 @@ function buildContent(opts) {
|
|
|
272
361
|
};
|
|
273
362
|
}
|
|
274
363
|
if (type === "list") {
|
|
275
|
-
const elements = opts.elements ?
|
|
364
|
+
const elements = opts.elements ? (() => {
|
|
365
|
+
const parsed = JSON.parse(opts.elements);
|
|
366
|
+
if (!Array.isArray(parsed)) throw new Error("elements must be a JSON array");
|
|
367
|
+
return parsed;
|
|
368
|
+
})() : [];
|
|
276
369
|
return {
|
|
277
370
|
type: "list_template",
|
|
278
371
|
coverData: { text: opts.text },
|
|
@@ -294,7 +387,7 @@ async function send(opts) {
|
|
|
294
387
|
if (opts.to) {
|
|
295
388
|
const result = await request({
|
|
296
389
|
method: "POST",
|
|
297
|
-
path: `/bots/${creds.botId}/users/${opts.to}/messages`,
|
|
390
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/users/${sanitizePathSegment(opts.to)}/messages`,
|
|
298
391
|
body,
|
|
299
392
|
profile
|
|
300
393
|
});
|
|
@@ -303,7 +396,7 @@ async function send(opts) {
|
|
|
303
396
|
if (opts.channel) {
|
|
304
397
|
const result = await request({
|
|
305
398
|
method: "POST",
|
|
306
|
-
path: `/bots/${creds.botId}/channels/${opts.channel}/messages`,
|
|
399
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(opts.channel)}/messages`,
|
|
307
400
|
body,
|
|
308
401
|
profile
|
|
309
402
|
});
|
|
@@ -320,7 +413,7 @@ async function listMembers(channelId, profile = "default") {
|
|
|
320
413
|
}
|
|
321
414
|
const result = await request({
|
|
322
415
|
method: "GET",
|
|
323
|
-
path: `/bots/${creds.botId}/channels/${channelId}/members`,
|
|
416
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(channelId)}/members`,
|
|
324
417
|
profile
|
|
325
418
|
});
|
|
326
419
|
return { members: result.members ?? [], responseMetaData: result.responseMetaData };
|
|
@@ -341,7 +434,8 @@ import { randomUUID } from "crypto";
|
|
|
341
434
|
|
|
342
435
|
// src/auth/oauth-user.ts
|
|
343
436
|
import { createServer } from "http";
|
|
344
|
-
import {
|
|
437
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
438
|
+
import { URL as URL2 } from "url";
|
|
345
439
|
var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
|
|
346
440
|
var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
|
|
347
441
|
var REDIRECT_PORT = 9876;
|
|
@@ -356,14 +450,14 @@ function buildAuthorizeUrl(clientId, scope, state) {
|
|
|
356
450
|
});
|
|
357
451
|
return `${AUTH_URL2}?${params.toString()}`;
|
|
358
452
|
}
|
|
359
|
-
function waitForAuthCode() {
|
|
360
|
-
return new Promise((
|
|
453
|
+
function waitForAuthCode(expectedState) {
|
|
454
|
+
return new Promise((resolve2, reject) => {
|
|
361
455
|
const timeout = setTimeout(() => {
|
|
362
456
|
server.close();
|
|
363
457
|
reject(new AuthError("OAuth login timed out (120s). Try again."));
|
|
364
458
|
}, 12e4);
|
|
365
459
|
const server = createServer((req, res) => {
|
|
366
|
-
const url = new
|
|
460
|
+
const url = new URL2(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
|
|
367
461
|
if (url.pathname !== "/callback") {
|
|
368
462
|
res.writeHead(404);
|
|
369
463
|
res.end("Not found");
|
|
@@ -371,6 +465,15 @@ function waitForAuthCode() {
|
|
|
371
465
|
}
|
|
372
466
|
const code = url.searchParams.get("code");
|
|
373
467
|
const error = url.searchParams.get("error");
|
|
468
|
+
const state = url.searchParams.get("state");
|
|
469
|
+
if (state !== expectedState) {
|
|
470
|
+
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
|
|
471
|
+
res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.</p>");
|
|
472
|
+
clearTimeout(timeout);
|
|
473
|
+
server.close();
|
|
474
|
+
reject(new AuthError("OAuth state mismatch \u2014 possible CSRF attack."));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
374
477
|
if (error) {
|
|
375
478
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
376
479
|
res.end("<h2>\uB85C\uADF8\uC778 \uC2E4\uD328</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>");
|
|
@@ -391,9 +494,9 @@ function waitForAuthCode() {
|
|
|
391
494
|
res.end("<h2>\uB85C\uADF8\uC778 \uC131\uACF5!</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uACE0 \uD130\uBBF8\uB110\uB85C \uB3CC\uC544\uAC00\uC138\uC694.</p>");
|
|
392
495
|
clearTimeout(timeout);
|
|
393
496
|
server.close();
|
|
394
|
-
|
|
497
|
+
resolve2(code);
|
|
395
498
|
});
|
|
396
|
-
server.listen(REDIRECT_PORT, () => {
|
|
499
|
+
server.listen(REDIRECT_PORT, "127.0.0.1", () => {
|
|
397
500
|
});
|
|
398
501
|
server.on("error", (err) => {
|
|
399
502
|
clearTimeout(timeout);
|
|
@@ -416,7 +519,8 @@ async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
|
416
519
|
});
|
|
417
520
|
if (!res.ok) {
|
|
418
521
|
const text = await res.text();
|
|
419
|
-
|
|
522
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
523
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
|
|
420
524
|
}
|
|
421
525
|
const data = await res.json();
|
|
422
526
|
return {
|
|
@@ -441,7 +545,8 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
441
545
|
});
|
|
442
546
|
if (!res.ok) {
|
|
443
547
|
const text = await res.text();
|
|
444
|
-
|
|
548
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
549
|
+
throw new AuthError(`Token refresh failed (${res.status}): ${truncated}`);
|
|
445
550
|
}
|
|
446
551
|
const data = await res.json();
|
|
447
552
|
return {
|
|
@@ -451,11 +556,28 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
451
556
|
scope: data.scope
|
|
452
557
|
};
|
|
453
558
|
}
|
|
454
|
-
function startOAuthCallbackServer(clientId, clientSecret) {
|
|
455
|
-
return waitForAuthCode().then(
|
|
559
|
+
function startOAuthCallbackServer(clientId, clientSecret, expectedState) {
|
|
560
|
+
return waitForAuthCode(expectedState).then(
|
|
456
561
|
(code) => exchangeCodeForToken(code, clientId, clientSecret)
|
|
457
562
|
);
|
|
458
563
|
}
|
|
564
|
+
async function revokeToken(token, clientId, clientSecret) {
|
|
565
|
+
try {
|
|
566
|
+
const res = await fetch("https://auth.worksmobile.com/oauth2/v2.0/revoke", {
|
|
567
|
+
method: "POST",
|
|
568
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
569
|
+
body: new URLSearchParams({
|
|
570
|
+
token,
|
|
571
|
+
client_id: clientId,
|
|
572
|
+
client_secret: clientSecret
|
|
573
|
+
}).toString()
|
|
574
|
+
});
|
|
575
|
+
if (!res.ok && process.env["NWORKS_VERBOSE"] === "1") {
|
|
576
|
+
console.error(`[nworks] Token revoke returned ${res.status}`);
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
459
581
|
|
|
460
582
|
// src/auth/token-user.ts
|
|
461
583
|
async function getValidUserToken(profile = "default") {
|
|
@@ -508,7 +630,7 @@ function normalizeDateTime(dt) {
|
|
|
508
630
|
async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
|
|
509
631
|
const from = encodeURIComponent(fromDateTime);
|
|
510
632
|
const until = encodeURIComponent(untilDateTime);
|
|
511
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
633
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
512
634
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
513
635
|
console.error(`[nworks] GET ${url}`);
|
|
514
636
|
}
|
|
@@ -545,10 +667,10 @@ async function createEvent(opts) {
|
|
|
545
667
|
eventComponents: [eventComponent],
|
|
546
668
|
sendNotification: opts.sendNotification ?? false
|
|
547
669
|
};
|
|
548
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events`;
|
|
670
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events`;
|
|
549
671
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
550
672
|
console.error(`[nworks] POST ${url}`);
|
|
551
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
673
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
552
674
|
}
|
|
553
675
|
const res = await authedFetch(
|
|
554
676
|
url,
|
|
@@ -566,7 +688,7 @@ async function createEvent(opts) {
|
|
|
566
688
|
return await res.json();
|
|
567
689
|
}
|
|
568
690
|
async function getEvent(eventId, userId = "me", profile = "default") {
|
|
569
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}`;
|
|
691
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}`;
|
|
570
692
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
571
693
|
console.error(`[nworks] GET ${url}`);
|
|
572
694
|
}
|
|
@@ -609,10 +731,10 @@ async function updateEvent(opts) {
|
|
|
609
731
|
eventComponents: [eventComponent],
|
|
610
732
|
sendNotification: opts.sendNotification ?? false
|
|
611
733
|
};
|
|
612
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${opts.eventId}`;
|
|
734
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(opts.eventId)}`;
|
|
613
735
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
614
736
|
console.error(`[nworks] PUT ${url}`);
|
|
615
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
737
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
616
738
|
}
|
|
617
739
|
const res = await authedFetch(
|
|
618
740
|
url,
|
|
@@ -628,7 +750,7 @@ async function updateEvent(opts) {
|
|
|
628
750
|
async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
|
|
629
751
|
const params = new URLSearchParams();
|
|
630
752
|
params.set("sendNotification", String(sendNotification));
|
|
631
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}?${params.toString()}`;
|
|
753
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}?${params.toString()}`;
|
|
632
754
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
633
755
|
console.error(`[nworks] DELETE ${url}`);
|
|
634
756
|
}
|
|
@@ -638,9 +760,15 @@ async function deleteEvent(eventId, userId = "me", sendNotification = false, pro
|
|
|
638
760
|
}
|
|
639
761
|
|
|
640
762
|
// src/api/drive.ts
|
|
641
|
-
import { readFile as readFile3, stat } from "fs/promises";
|
|
642
|
-
import { basename } from "path";
|
|
763
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
764
|
+
import { basename as basename2 } from "path";
|
|
643
765
|
var BASE_URL3 = "https://www.worksapis.com/v1.0";
|
|
766
|
+
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
767
|
+
var ALLOWED_HOSTS = [
|
|
768
|
+
"storage.worksmobile.com",
|
|
769
|
+
"www.worksapis.com",
|
|
770
|
+
"worksapis.com"
|
|
771
|
+
];
|
|
644
772
|
async function authedFetch2(url, init, profile) {
|
|
645
773
|
const token = await getValidUserToken(profile);
|
|
646
774
|
const headers = new Headers(init.headers);
|
|
@@ -662,8 +790,8 @@ async function handleError2(res) {
|
|
|
662
790
|
throw new ApiError(code, description, res.status);
|
|
663
791
|
}
|
|
664
792
|
async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
|
|
665
|
-
const base = `${BASE_URL3}/users/${userId}/drive/files`;
|
|
666
|
-
const path = folderId ? `${base}/${folderId}/children` : base;
|
|
793
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
794
|
+
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
667
795
|
const params = new URLSearchParams();
|
|
668
796
|
params.set("count", String(count));
|
|
669
797
|
if (cursor) params.set("cursor", cursor);
|
|
@@ -677,11 +805,15 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
|
|
|
677
805
|
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
678
806
|
}
|
|
679
807
|
async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
680
|
-
const fileName =
|
|
681
|
-
const
|
|
808
|
+
const fileName = basename2(localPath);
|
|
809
|
+
const safeName = sanitizeFileName(fileName);
|
|
810
|
+
const fileStat = await stat2(localPath);
|
|
682
811
|
const fileSize = fileStat.size;
|
|
683
|
-
|
|
684
|
-
|
|
812
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
813
|
+
throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
|
|
814
|
+
}
|
|
815
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
816
|
+
const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
|
|
685
817
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
686
818
|
console.error(`[nworks] POST ${createUrl} (create upload URL)`);
|
|
687
819
|
}
|
|
@@ -696,11 +828,12 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
696
828
|
);
|
|
697
829
|
if (!createRes.ok) return handleError2(createRes);
|
|
698
830
|
const { uploadUrl } = await createRes.json();
|
|
831
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
699
832
|
const fileBuffer = await readFile3(localPath);
|
|
700
833
|
const boundary = `----nworks${Date.now()}`;
|
|
701
834
|
const header = Buffer.from(
|
|
702
835
|
`--${boundary}\r
|
|
703
|
-
Content-Disposition: form-data; name="Filedata"; filename="${
|
|
836
|
+
Content-Disposition: form-data; name="Filedata"; filename="${safeName}"\r
|
|
704
837
|
Content-Type: application/octet-stream\r
|
|
705
838
|
\r
|
|
706
839
|
`
|
|
@@ -726,8 +859,11 @@ Content-Type: application/octet-stream\r
|
|
|
726
859
|
}
|
|
727
860
|
async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
728
861
|
const fileSize = fileBuffer.length;
|
|
729
|
-
|
|
730
|
-
|
|
862
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
863
|
+
throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
|
|
864
|
+
}
|
|
865
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
866
|
+
const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
|
|
731
867
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
732
868
|
console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
|
|
733
869
|
}
|
|
@@ -742,10 +878,11 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
742
878
|
);
|
|
743
879
|
if (!createRes.ok) return handleError2(createRes);
|
|
744
880
|
const { uploadUrl } = await createRes.json();
|
|
881
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
745
882
|
const boundary = `----nworks${Date.now()}`;
|
|
746
883
|
const header = Buffer.from(
|
|
747
884
|
`--${boundary}\r
|
|
748
|
-
Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
|
|
885
|
+
Content-Disposition: form-data; name="Filedata"; filename="${sanitizeFileName(fileName)}"\r
|
|
749
886
|
Content-Type: application/octet-stream\r
|
|
750
887
|
\r
|
|
751
888
|
`
|
|
@@ -770,7 +907,7 @@ Content-Type: application/octet-stream\r
|
|
|
770
907
|
return await uploadRes.json();
|
|
771
908
|
}
|
|
772
909
|
async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
773
|
-
const url = `${BASE_URL3}/users/${userId}/drive/files/${fileId}/download`;
|
|
910
|
+
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
774
911
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
775
912
|
console.error(`[nworks] GET ${url} (get download URL)`);
|
|
776
913
|
}
|
|
@@ -787,10 +924,11 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
|
787
924
|
if (!redirectRes.ok) return handleError2(redirectRes);
|
|
788
925
|
throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
|
|
789
926
|
}
|
|
927
|
+
const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
|
|
790
928
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
791
|
-
console.error(`[nworks] GET ${
|
|
929
|
+
console.error(`[nworks] GET ${safeLocation} (download content)`);
|
|
792
930
|
}
|
|
793
|
-
const downloadRes = await
|
|
931
|
+
const downloadRes = await fetch(safeLocation, { method: "GET" });
|
|
794
932
|
if (!downloadRes.ok) return handleError2(downloadRes);
|
|
795
933
|
const arrayBuffer = await downloadRes.arrayBuffer();
|
|
796
934
|
const buffer = Buffer.from(arrayBuffer);
|
|
@@ -830,7 +968,7 @@ async function handleError3(res) {
|
|
|
830
968
|
async function sendMail(opts) {
|
|
831
969
|
const userId = opts.userId ?? "me";
|
|
832
970
|
const profile = opts.profile ?? "default";
|
|
833
|
-
const url = `${BASE_URL4}/users/${userId}/mail`;
|
|
971
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail`;
|
|
834
972
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
835
973
|
console.error(`[nworks] POST ${url}`);
|
|
836
974
|
}
|
|
@@ -859,7 +997,7 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
859
997
|
params.set("count", String(count));
|
|
860
998
|
if (cursor) params.set("cursor", cursor);
|
|
861
999
|
if (isUnread) params.set("isUnread", "true");
|
|
862
|
-
const url = `${BASE_URL4}/users/${userId}/mail/mailfolders/${folderId}/children?${params.toString()}`;
|
|
1000
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/mailfolders/${sanitizePathSegment(String(folderId))}/children?${params.toString()}`;
|
|
863
1001
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
864
1002
|
console.error(`[nworks] GET ${url}`);
|
|
865
1003
|
}
|
|
@@ -875,7 +1013,7 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
875
1013
|
};
|
|
876
1014
|
}
|
|
877
1015
|
async function readMail(mailId, userId = "me", profile = "default") {
|
|
878
|
-
const url = `${BASE_URL4}/users/${userId}/mail/${mailId}`;
|
|
1016
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}`;
|
|
879
1017
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
880
1018
|
console.error(`[nworks] GET ${url}`);
|
|
881
1019
|
}
|
|
@@ -908,7 +1046,7 @@ async function handleError4(res) {
|
|
|
908
1046
|
}
|
|
909
1047
|
async function resolveUserId(userId, profile) {
|
|
910
1048
|
if (userId !== "me") return userId;
|
|
911
|
-
const url = `${BASE_URL5}/users
|
|
1049
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
|
|
912
1050
|
const res = await authedFetch4(url, { method: "GET" }, profile);
|
|
913
1051
|
if (!res.ok) return handleError4(res);
|
|
914
1052
|
const data = await res.json();
|
|
@@ -923,7 +1061,7 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
|
|
|
923
1061
|
params.set("count", String(count));
|
|
924
1062
|
params.set("status", status);
|
|
925
1063
|
if (cursor) params.set("cursor", cursor);
|
|
926
|
-
const url = `${BASE_URL5}/users/${userId}/tasks?${params.toString()}`;
|
|
1064
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks?${params.toString()}`;
|
|
927
1065
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
928
1066
|
console.error(`[nworks] GET ${url}`);
|
|
929
1067
|
}
|
|
@@ -947,10 +1085,10 @@ async function createTask(opts) {
|
|
|
947
1085
|
};
|
|
948
1086
|
if (opts.dueDate) body.dueDate = opts.dueDate;
|
|
949
1087
|
if (opts.categoryId) body.categoryId = opts.categoryId;
|
|
950
|
-
const url = `${BASE_URL5}/users/${userId}/tasks`;
|
|
1088
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks`;
|
|
951
1089
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
952
1090
|
console.error(`[nworks] POST ${url}`);
|
|
953
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1091
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
954
1092
|
}
|
|
955
1093
|
const res = await authedFetch4(
|
|
956
1094
|
url,
|
|
@@ -973,7 +1111,7 @@ async function updateTask(opts) {
|
|
|
973
1111
|
if (opts.title !== void 0) body.title = opts.title;
|
|
974
1112
|
if (opts.content !== void 0) body.content = opts.content;
|
|
975
1113
|
if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
|
|
976
|
-
const url = `${BASE_URL5}/tasks/${opts.taskId}`;
|
|
1114
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(opts.taskId)}`;
|
|
977
1115
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
978
1116
|
console.error(`[nworks] PATCH ${url}`);
|
|
979
1117
|
}
|
|
@@ -990,7 +1128,7 @@ async function updateTask(opts) {
|
|
|
990
1128
|
return await res.json();
|
|
991
1129
|
}
|
|
992
1130
|
async function completeTask(taskId, profile = "default") {
|
|
993
|
-
const url = `${BASE_URL5}/tasks/${taskId}/complete`;
|
|
1131
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/complete`;
|
|
994
1132
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
995
1133
|
console.error(`[nworks] POST ${url}`);
|
|
996
1134
|
}
|
|
@@ -1003,7 +1141,7 @@ async function completeTask(taskId, profile = "default") {
|
|
|
1003
1141
|
if (!res.ok) return handleError4(res);
|
|
1004
1142
|
}
|
|
1005
1143
|
async function incompleteTask(taskId, profile = "default") {
|
|
1006
|
-
const url = `${BASE_URL5}/tasks/${taskId}/incomplete`;
|
|
1144
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
|
|
1007
1145
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1008
1146
|
console.error(`[nworks] POST ${url}`);
|
|
1009
1147
|
}
|
|
@@ -1016,7 +1154,7 @@ async function incompleteTask(taskId, profile = "default") {
|
|
|
1016
1154
|
if (!res.ok) return handleError4(res);
|
|
1017
1155
|
}
|
|
1018
1156
|
async function deleteTask(taskId, profile = "default") {
|
|
1019
|
-
const url = `${BASE_URL5}/tasks/${taskId}`;
|
|
1157
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
|
|
1020
1158
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1021
1159
|
console.error(`[nworks] DELETE ${url}`);
|
|
1022
1160
|
}
|
|
@@ -1070,7 +1208,7 @@ async function listBoards(count = 20, cursor, profile = "default") {
|
|
|
1070
1208
|
if (!res.ok) return handleError5(res);
|
|
1071
1209
|
const text = await res.text();
|
|
1072
1210
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1073
|
-
console.error(`[nworks] Response: ${text}`);
|
|
1211
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1074
1212
|
}
|
|
1075
1213
|
const data = safeParseJson(text);
|
|
1076
1214
|
return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
|
|
@@ -1079,7 +1217,7 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1079
1217
|
const params = new URLSearchParams();
|
|
1080
1218
|
params.set("count", String(count));
|
|
1081
1219
|
if (cursor) params.set("cursor", cursor);
|
|
1082
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts?${params.toString()}`;
|
|
1220
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts?${params.toString()}`;
|
|
1083
1221
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1084
1222
|
console.error(`[nworks] GET ${url}`);
|
|
1085
1223
|
}
|
|
@@ -1087,13 +1225,13 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1087
1225
|
if (!res.ok) return handleError5(res);
|
|
1088
1226
|
const text = await res.text();
|
|
1089
1227
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1090
|
-
console.error(`[nworks] Response: ${text}`);
|
|
1228
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1091
1229
|
}
|
|
1092
1230
|
const data = safeParseJson(text);
|
|
1093
1231
|
return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
|
|
1094
1232
|
}
|
|
1095
1233
|
async function readPost(boardId, postId, profile = "default") {
|
|
1096
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts/${postId}`;
|
|
1234
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts/${sanitizePathSegment(postId)}`;
|
|
1097
1235
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1098
1236
|
console.error(`[nworks] GET ${url}`);
|
|
1099
1237
|
}
|
|
@@ -1101,7 +1239,7 @@ async function readPost(boardId, postId, profile = "default") {
|
|
|
1101
1239
|
if (!res.ok) return handleError5(res);
|
|
1102
1240
|
const text = await res.text();
|
|
1103
1241
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1104
|
-
console.error(`[nworks] Response: ${text}`);
|
|
1242
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1105
1243
|
}
|
|
1106
1244
|
return safeParseJson(text);
|
|
1107
1245
|
}
|
|
@@ -1113,10 +1251,10 @@ async function createPost(opts) {
|
|
|
1113
1251
|
};
|
|
1114
1252
|
if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
|
|
1115
1253
|
if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
|
|
1116
|
-
const url = `${BASE_URL6}/boards/${opts.boardId}/posts`;
|
|
1254
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(opts.boardId)}/posts`;
|
|
1117
1255
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1118
1256
|
console.error(`[nworks] POST ${url}`);
|
|
1119
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1257
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1120
1258
|
}
|
|
1121
1259
|
const res = await authedFetch5(
|
|
1122
1260
|
url,
|
|
@@ -1130,7 +1268,7 @@ async function createPost(opts) {
|
|
|
1130
1268
|
if (res.status === 201 || res.ok) {
|
|
1131
1269
|
const text = await res.text();
|
|
1132
1270
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1133
|
-
console.error(`[nworks] Response: ${text}`);
|
|
1271
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1134
1272
|
}
|
|
1135
1273
|
return safeParseJson(text);
|
|
1136
1274
|
}
|
|
@@ -1506,7 +1644,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
1506
1644
|
success: true,
|
|
1507
1645
|
message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
1508
1646
|
nextSteps,
|
|
1509
|
-
clientId,
|
|
1647
|
+
clientId: mask(clientId),
|
|
1510
1648
|
clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
|
|
1511
1649
|
serviceAccount: serviceAccount ?? null,
|
|
1512
1650
|
privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
|
|
@@ -1642,7 +1780,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
1642
1780
|
sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
1643
1781
|
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
1644
1782
|
},
|
|
1645
|
-
async ({
|
|
1783
|
+
async ({
|
|
1784
|
+
summary,
|
|
1785
|
+
start,
|
|
1786
|
+
end,
|
|
1787
|
+
timeZone,
|
|
1788
|
+
description,
|
|
1789
|
+
location,
|
|
1790
|
+
attendees,
|
|
1791
|
+
sendNotification,
|
|
1792
|
+
userId
|
|
1793
|
+
}) => {
|
|
1646
1794
|
try {
|
|
1647
1795
|
const result = await createEvent({
|
|
1648
1796
|
summary,
|
|
@@ -1681,7 +1829,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
1681
1829
|
sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
1682
1830
|
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
1683
1831
|
},
|
|
1684
|
-
async ({
|
|
1832
|
+
async ({
|
|
1833
|
+
eventId,
|
|
1834
|
+
summary,
|
|
1835
|
+
start,
|
|
1836
|
+
end,
|
|
1837
|
+
timeZone,
|
|
1838
|
+
description,
|
|
1839
|
+
location,
|
|
1840
|
+
sendNotification,
|
|
1841
|
+
userId
|
|
1842
|
+
}) => {
|
|
1685
1843
|
try {
|
|
1686
1844
|
await updateEvent({
|
|
1687
1845
|
eventId,
|
|
@@ -1794,11 +1952,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
1794
1952
|
overwrite ?? false
|
|
1795
1953
|
);
|
|
1796
1954
|
} else if (filePath) {
|
|
1955
|
+
const safePath = validateLocalPath(filePath);
|
|
1797
1956
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1798
|
-
console.error(`[nworks] MCP upload: filePath=${
|
|
1957
|
+
console.error(`[nworks] MCP upload: filePath=${safePath}`);
|
|
1799
1958
|
}
|
|
1800
1959
|
result = await uploadFile(
|
|
1801
|
-
|
|
1960
|
+
safePath,
|
|
1802
1961
|
userId ?? "me",
|
|
1803
1962
|
folderId,
|
|
1804
1963
|
overwrite ?? false
|
|
@@ -1813,10 +1972,11 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
1813
1972
|
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
1814
1973
|
};
|
|
1815
1974
|
} catch (err) {
|
|
1816
|
-
|
|
1817
|
-
|
|
1975
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1976
|
+
console.error(`[nworks] drive upload error: ${err.stack}`);
|
|
1977
|
+
}
|
|
1818
1978
|
return {
|
|
1819
|
-
content: [{ type: "text", text:
|
|
1979
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
|
|
1820
1980
|
isError: true
|
|
1821
1981
|
};
|
|
1822
1982
|
}
|
|
@@ -1841,7 +2001,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
1841
2001
|
if (outputDir) {
|
|
1842
2002
|
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
1843
2003
|
const { join: join2 } = await import("path");
|
|
1844
|
-
const
|
|
2004
|
+
const safeDir = validateLocalPath(outputDir);
|
|
2005
|
+
const safeName = sanitizeFileName(fileName);
|
|
2006
|
+
const outPath = join2(safeDir, safeName);
|
|
2007
|
+
validateLocalPath(outPath, safeDir);
|
|
1845
2008
|
await writeFile2(outPath, result.buffer);
|
|
1846
2009
|
return {
|
|
1847
2010
|
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
@@ -2262,9 +2425,9 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2262
2425
|
const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
2263
2426
|
const requestedScopes = expandScopes((scope ?? DEFAULT_SCOPE).split(" ").filter(Boolean));
|
|
2264
2427
|
const mergedScopes = [.../* @__PURE__ */ new Set([...existingScopes, ...requestedScopes])].join(" ");
|
|
2265
|
-
const state =
|
|
2428
|
+
const state = generateSecureState();
|
|
2266
2429
|
const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
|
|
2267
|
-
startOAuthCallbackServer(creds.clientId, creds.clientSecret).then(
|
|
2430
|
+
startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
|
|
2268
2431
|
(token) => saveUserToken({
|
|
2269
2432
|
accessToken: token.accessToken,
|
|
2270
2433
|
refreshToken: token.refreshToken,
|
|
@@ -2309,9 +2472,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2309
2472
|
const userToken = await loadUserToken();
|
|
2310
2473
|
const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
|
|
2311
2474
|
const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
|
|
2475
|
+
const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
2312
2476
|
const info = {
|
|
2313
2477
|
serviceAccount: creds.serviceAccount ?? null,
|
|
2314
|
-
clientId: creds.clientId,
|
|
2478
|
+
clientId: mask(creds.clientId),
|
|
2315
2479
|
botId: creds.botId ?? null,
|
|
2316
2480
|
tokenValid: isValid,
|
|
2317
2481
|
userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
|
|
@@ -2333,6 +2497,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2333
2497
|
{},
|
|
2334
2498
|
async () => {
|
|
2335
2499
|
try {
|
|
2500
|
+
try {
|
|
2501
|
+
const creds = await loadCredentials();
|
|
2502
|
+
const token = await loadToken();
|
|
2503
|
+
const userToken = await loadUserToken();
|
|
2504
|
+
if (token?.accessToken) {
|
|
2505
|
+
await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
|
|
2506
|
+
}
|
|
2507
|
+
if (userToken?.refreshToken) {
|
|
2508
|
+
await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
|
|
2509
|
+
}
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2336
2512
|
await clearCredentials();
|
|
2337
2513
|
return {
|
|
2338
2514
|
content: [
|
|
@@ -2360,8 +2536,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2360
2536
|
async () => {
|
|
2361
2537
|
try {
|
|
2362
2538
|
const results = await runChecks("default");
|
|
2539
|
+
const maskedResults = results.map((r) => {
|
|
2540
|
+
if (r.check === "credentials" && r.status === "OK") {
|
|
2541
|
+
return { ...r, detail: r.detail.replace(/clientId: .+/, "clientId: ****") };
|
|
2542
|
+
}
|
|
2543
|
+
if (r.check === "privateKey" && r.status === "OK") {
|
|
2544
|
+
return { ...r, detail: "OK (path hidden)" };
|
|
2545
|
+
}
|
|
2546
|
+
if (r.check === "serviceAccount" && r.status === "OK") {
|
|
2547
|
+
const masked = r.detail.length <= 4 ? "****" : `****${r.detail.slice(-4)}`;
|
|
2548
|
+
return { ...r, detail: masked };
|
|
2549
|
+
}
|
|
2550
|
+
return r;
|
|
2551
|
+
});
|
|
2363
2552
|
return {
|
|
2364
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2553
|
+
content: [{ type: "text", text: JSON.stringify(maskedResults) }]
|
|
2365
2554
|
};
|
|
2366
2555
|
} catch (err) {
|
|
2367
2556
|
return {
|