nworks 1.2.0 → 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 +324 -105
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +302 -99
- package/dist/mcp.js.map +1 -1
- package/package.json +58 -58
package/dist/index.js
CHANGED
|
@@ -38,13 +38,14 @@ var ApiError = class extends Error {
|
|
|
38
38
|
function hasServiceAccountCreds(creds) {
|
|
39
39
|
return !!(creds.serviceAccount && creds.privateKeyPath && creds.botId);
|
|
40
40
|
}
|
|
41
|
+
var IS_UNIX = process.platform !== "win32";
|
|
41
42
|
var CONFIG_DIR = join(homedir(), ".config", "nworks");
|
|
42
43
|
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
43
44
|
var TOKEN_PATH = join(CONFIG_DIR, "token.json");
|
|
44
45
|
var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
|
|
45
46
|
async function ensureConfigDir() {
|
|
46
47
|
if (!existsSync(CONFIG_DIR)) {
|
|
47
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
48
|
+
await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
function getCredentialsFromEnv() {
|
|
@@ -84,7 +85,11 @@ async function saveCredentials(creds, profile = "default") {
|
|
|
84
85
|
profiles = JSON.parse(raw);
|
|
85
86
|
}
|
|
86
87
|
profiles[profile] = creds;
|
|
87
|
-
await writeFile(
|
|
88
|
+
await writeFile(
|
|
89
|
+
CREDENTIALS_PATH,
|
|
90
|
+
JSON.stringify(profiles, null, 2),
|
|
91
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
92
|
+
);
|
|
88
93
|
}
|
|
89
94
|
async function loadToken(profile = "default") {
|
|
90
95
|
if (!existsSync(TOKEN_PATH)) return null;
|
|
@@ -105,7 +110,11 @@ async function saveToken(token, profile = "default") {
|
|
|
105
110
|
tokens = JSON.parse(raw);
|
|
106
111
|
}
|
|
107
112
|
tokens[profile] = token;
|
|
108
|
-
await writeFile(
|
|
113
|
+
await writeFile(
|
|
114
|
+
TOKEN_PATH,
|
|
115
|
+
JSON.stringify(tokens, null, 2),
|
|
116
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
117
|
+
);
|
|
109
118
|
}
|
|
110
119
|
async function loadUserToken(profile = "default") {
|
|
111
120
|
if (!existsSync(USER_TOKEN_PATH)) return null;
|
|
@@ -128,7 +137,11 @@ async function saveUserToken(token, profile = "default") {
|
|
|
128
137
|
tokens = JSON.parse(raw);
|
|
129
138
|
}
|
|
130
139
|
tokens[profile] = token;
|
|
131
|
-
await writeFile(
|
|
140
|
+
await writeFile(
|
|
141
|
+
USER_TOKEN_PATH,
|
|
142
|
+
JSON.stringify(tokens, null, 2),
|
|
143
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
144
|
+
);
|
|
132
145
|
}
|
|
133
146
|
async function clearCredentials(profile = "default") {
|
|
134
147
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
@@ -138,25 +151,33 @@ async function clearCredentials(profile = "default") {
|
|
|
138
151
|
await writeFile(
|
|
139
152
|
CREDENTIALS_PATH,
|
|
140
153
|
JSON.stringify(profiles, null, 2),
|
|
141
|
-
"utf-8"
|
|
154
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
142
155
|
);
|
|
143
156
|
}
|
|
144
157
|
if (existsSync(TOKEN_PATH)) {
|
|
145
158
|
const raw = await readFile(TOKEN_PATH, "utf-8");
|
|
146
159
|
const tokens = JSON.parse(raw);
|
|
147
160
|
delete tokens[profile];
|
|
148
|
-
await writeFile(
|
|
161
|
+
await writeFile(
|
|
162
|
+
TOKEN_PATH,
|
|
163
|
+
JSON.stringify(tokens, null, 2),
|
|
164
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
165
|
+
);
|
|
149
166
|
}
|
|
150
167
|
if (existsSync(USER_TOKEN_PATH)) {
|
|
151
168
|
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
152
169
|
const tokens = JSON.parse(raw);
|
|
153
170
|
delete tokens[profile];
|
|
154
|
-
await writeFile(
|
|
171
|
+
await writeFile(
|
|
172
|
+
USER_TOKEN_PATH,
|
|
173
|
+
JSON.stringify(tokens, null, 2),
|
|
174
|
+
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
175
|
+
);
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
// src/auth/jwt.ts
|
|
159
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
180
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
160
181
|
import jwt from "jsonwebtoken";
|
|
161
182
|
async function createJWT(creds) {
|
|
162
183
|
if (!creds.serviceAccount || !creds.privateKeyPath) {
|
|
@@ -165,6 +186,15 @@ async function createJWT(creds) {
|
|
|
165
186
|
);
|
|
166
187
|
}
|
|
167
188
|
const privateKey = await readFile2(creds.privateKeyPath, "utf-8");
|
|
189
|
+
if (process.platform !== "win32") {
|
|
190
|
+
const fileStat = await stat(creds.privateKeyPath);
|
|
191
|
+
const mode = fileStat.mode & 511;
|
|
192
|
+
if (mode & 63) {
|
|
193
|
+
console.error(
|
|
194
|
+
`[nworks] Warning: Private key file has permissions ${mode.toString(8)}. Recommended: 600`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
168
198
|
const now = Math.floor(Date.now() / 1e3);
|
|
169
199
|
const payload = {
|
|
170
200
|
iss: creds.clientId,
|
|
@@ -201,7 +231,8 @@ async function refreshToken(profile = "default") {
|
|
|
201
231
|
});
|
|
202
232
|
if (!res.ok) {
|
|
203
233
|
const text = await res.text();
|
|
204
|
-
|
|
234
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
235
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
|
|
205
236
|
}
|
|
206
237
|
const data = await res.json();
|
|
207
238
|
const expiresIn = Number(data.expires_in);
|
|
@@ -215,7 +246,8 @@ async function refreshToken(profile = "default") {
|
|
|
215
246
|
|
|
216
247
|
// src/auth/oauth-user.ts
|
|
217
248
|
import { createServer } from "http";
|
|
218
|
-
import {
|
|
249
|
+
import { randomBytes } from "crypto";
|
|
250
|
+
import { URL as URL2 } from "url";
|
|
219
251
|
var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
|
|
220
252
|
var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
|
|
221
253
|
var REDIRECT_PORT = 9876;
|
|
@@ -230,19 +262,19 @@ function buildAuthorizeUrl(clientId, scope, state) {
|
|
|
230
262
|
});
|
|
231
263
|
return `${AUTH_URL2}?${params.toString()}`;
|
|
232
264
|
}
|
|
233
|
-
async function startUserOAuthFlow(_scope, profile = "default") {
|
|
265
|
+
async function startUserOAuthFlow(_scope, profile = "default", expectedState) {
|
|
234
266
|
const creds = await loadCredentials(profile);
|
|
235
|
-
const code = await waitForAuthCode();
|
|
267
|
+
const code = await waitForAuthCode(expectedState ?? randomBytes(16).toString("hex"));
|
|
236
268
|
return exchangeCodeForToken(code, creds.clientId, creds.clientSecret);
|
|
237
269
|
}
|
|
238
|
-
function waitForAuthCode() {
|
|
239
|
-
return new Promise((
|
|
270
|
+
function waitForAuthCode(expectedState) {
|
|
271
|
+
return new Promise((resolve2, reject) => {
|
|
240
272
|
const timeout = setTimeout(() => {
|
|
241
273
|
server.close();
|
|
242
274
|
reject(new AuthError("OAuth login timed out (120s). Try again."));
|
|
243
275
|
}, 12e4);
|
|
244
276
|
const server = createServer((req, res) => {
|
|
245
|
-
const url = new
|
|
277
|
+
const url = new URL2(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
|
|
246
278
|
if (url.pathname !== "/callback") {
|
|
247
279
|
res.writeHead(404);
|
|
248
280
|
res.end("Not found");
|
|
@@ -250,6 +282,15 @@ function waitForAuthCode() {
|
|
|
250
282
|
}
|
|
251
283
|
const code = url.searchParams.get("code");
|
|
252
284
|
const error = url.searchParams.get("error");
|
|
285
|
+
const state = url.searchParams.get("state");
|
|
286
|
+
if (state !== expectedState) {
|
|
287
|
+
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
|
|
288
|
+
res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.</p>");
|
|
289
|
+
clearTimeout(timeout);
|
|
290
|
+
server.close();
|
|
291
|
+
reject(new AuthError("OAuth state mismatch \u2014 possible CSRF attack."));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
253
294
|
if (error) {
|
|
254
295
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
255
296
|
res.end("<h2>\uB85C\uADF8\uC778 \uC2E4\uD328</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>");
|
|
@@ -270,9 +311,9 @@ function waitForAuthCode() {
|
|
|
270
311
|
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>");
|
|
271
312
|
clearTimeout(timeout);
|
|
272
313
|
server.close();
|
|
273
|
-
|
|
314
|
+
resolve2(code);
|
|
274
315
|
});
|
|
275
|
-
server.listen(REDIRECT_PORT, () => {
|
|
316
|
+
server.listen(REDIRECT_PORT, "127.0.0.1", () => {
|
|
276
317
|
});
|
|
277
318
|
server.on("error", (err) => {
|
|
278
319
|
clearTimeout(timeout);
|
|
@@ -295,7 +336,8 @@ async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
|
295
336
|
});
|
|
296
337
|
if (!res.ok) {
|
|
297
338
|
const text = await res.text();
|
|
298
|
-
|
|
339
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
340
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
|
|
299
341
|
}
|
|
300
342
|
const data = await res.json();
|
|
301
343
|
return {
|
|
@@ -320,7 +362,8 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
320
362
|
});
|
|
321
363
|
if (!res.ok) {
|
|
322
364
|
const text = await res.text();
|
|
323
|
-
|
|
365
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
366
|
+
throw new AuthError(`Token refresh failed (${res.status}): ${truncated}`);
|
|
324
367
|
}
|
|
325
368
|
const data = await res.json();
|
|
326
369
|
return {
|
|
@@ -330,11 +373,28 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
330
373
|
scope: data.scope
|
|
331
374
|
};
|
|
332
375
|
}
|
|
333
|
-
function startOAuthCallbackServer(clientId, clientSecret) {
|
|
334
|
-
return waitForAuthCode().then(
|
|
376
|
+
function startOAuthCallbackServer(clientId, clientSecret, expectedState) {
|
|
377
|
+
return waitForAuthCode(expectedState).then(
|
|
335
378
|
(code) => exchangeCodeForToken(code, clientId, clientSecret)
|
|
336
379
|
);
|
|
337
380
|
}
|
|
381
|
+
async function revokeToken(token, clientId, clientSecret) {
|
|
382
|
+
try {
|
|
383
|
+
const res = await fetch("https://auth.worksmobile.com/oauth2/v2.0/revoke", {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
386
|
+
body: new URLSearchParams({
|
|
387
|
+
token,
|
|
388
|
+
client_id: clientId,
|
|
389
|
+
client_secret: clientSecret
|
|
390
|
+
}).toString()
|
|
391
|
+
});
|
|
392
|
+
if (!res.ok && process.env["NWORKS_VERBOSE"] === "1") {
|
|
393
|
+
console.error(`[nworks] Token revoke returned ${res.status}`);
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
338
398
|
|
|
339
399
|
// src/output/format.ts
|
|
340
400
|
function output(data, opts = {}) {
|
|
@@ -409,7 +469,7 @@ function errorOutput(error, opts = {}) {
|
|
|
409
469
|
}
|
|
410
470
|
|
|
411
471
|
// src/commands/login.ts
|
|
412
|
-
import { randomBytes } from "crypto";
|
|
472
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
413
473
|
async function prompt(question) {
|
|
414
474
|
const rl = createInterface({
|
|
415
475
|
input: process.stdin,
|
|
@@ -460,7 +520,7 @@ async function handleUserLogin(scope, profile, opts) {
|
|
|
460
520
|
} catch {
|
|
461
521
|
await saveCredentials({ clientId, clientSecret }, profile);
|
|
462
522
|
}
|
|
463
|
-
const state =
|
|
523
|
+
const state = randomBytes2(16).toString("hex");
|
|
464
524
|
const authorizeUrl = buildAuthorizeUrl(clientId, scope, state);
|
|
465
525
|
console.error(`
|
|
466
526
|
Opening browser for NAVER WORKS login...`);
|
|
@@ -470,8 +530,12 @@ Opening browser for NAVER WORKS login...`);
|
|
|
470
530
|
`);
|
|
471
531
|
const { exec } = await import("child_process");
|
|
472
532
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
473
|
-
|
|
474
|
-
|
|
533
|
+
if (process.platform === "win32") {
|
|
534
|
+
exec(`start "" "${authorizeUrl}"`);
|
|
535
|
+
} else {
|
|
536
|
+
exec(`${openCmd} "${authorizeUrl}"`);
|
|
537
|
+
}
|
|
538
|
+
const tokenData = await startUserOAuthFlow(scope, profile, state);
|
|
475
539
|
await saveUserToken(tokenData, profile);
|
|
476
540
|
output(
|
|
477
541
|
{
|
|
@@ -569,7 +633,7 @@ var ERROR_HINTS_MCP = {
|
|
|
569
633
|
FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
570
634
|
ACCESS_DENIED: "\uC811\uADFC\uC774 \uAC70\uBD80\uB410\uC2B5\uB2C8\uB2E4. Admin\uC5D0\uC11C Bot\uC744 \uCD94\uAC00\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
|
|
571
635
|
SERVICE_ACCOUNT_NOT_ALLOWED: "\uC11C\uBE44\uC2A4 \uACC4\uC815\uC73C\uB85C\uB294 \uC774 API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C User OAuth \uB85C\uADF8\uC778\uC744 \uBA3C\uC800 \uD574\uC8FC\uC138\uC694.",
|
|
572
|
-
UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. nworks_setup tool\uB85C \uC7AC\uC124\uC815\uD558\uC138\uC694."
|
|
636
|
+
UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB2E4\uC2DC \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uADF8\uB798\uB3C4 \uC548 \uB418\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."
|
|
573
637
|
};
|
|
574
638
|
function cliErrorHint(err, area) {
|
|
575
639
|
if (err instanceof ApiError) {
|
|
@@ -601,7 +665,7 @@ function mcpErrorHint(err, area) {
|
|
|
601
665
|
if (err instanceof AuthError) {
|
|
602
666
|
return `Error: ${err.message}
|
|
603
667
|
|
|
604
|
-
[\uC548\uB0B4] \uC778\uC99D \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_setup tool\
|
|
668
|
+
[\uC548\uB0B4] \uC778\uC99D \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_setup tool\uC744 \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694 (Client ID \uD544\uC694, Client Secret\uC740 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC77D\uC74C). \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C MCP \uC124\uC815 \uD30C\uC77C\uC758 env \uD544\uB4DC\uC5D0 NWORKS_CLIENT_SECRET \uCD94\uAC00\uB97C \uC548\uB0B4\uD558\uC138\uC694.`;
|
|
605
669
|
}
|
|
606
670
|
return `Error: ${err.message}`;
|
|
607
671
|
}
|
|
@@ -648,6 +712,18 @@ function cliError(err, opts = {}, area) {
|
|
|
648
712
|
var logoutCommand = new Command2("logout").description("Remove stored credentials and tokens").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
649
713
|
try {
|
|
650
714
|
const profile = opts.profile;
|
|
715
|
+
try {
|
|
716
|
+
const creds = await loadCredentials(profile);
|
|
717
|
+
const token = await loadToken(profile);
|
|
718
|
+
const userToken = await loadUserToken(profile);
|
|
719
|
+
if (token?.accessToken) {
|
|
720
|
+
await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
|
|
721
|
+
}
|
|
722
|
+
if (userToken?.refreshToken) {
|
|
723
|
+
await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
651
727
|
await clearCredentials(profile);
|
|
652
728
|
output({ success: true, message: `Logged out (profile: ${profile})` }, opts);
|
|
653
729
|
} catch (err) {
|
|
@@ -692,7 +768,7 @@ import { Command as Command4 } from "commander";
|
|
|
692
768
|
var BASE_URL = "https://www.worksapis.com/v1.0";
|
|
693
769
|
var MAX_RETRIES = 3;
|
|
694
770
|
function sleep(ms) {
|
|
695
|
-
return new Promise((
|
|
771
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
696
772
|
}
|
|
697
773
|
async function request(opts, _retryCount = 0) {
|
|
698
774
|
const { method, path, body, profile = "default" } = opts;
|
|
@@ -714,7 +790,9 @@ async function request(opts, _retryCount = 0) {
|
|
|
714
790
|
return request(opts, _retryCount + 1);
|
|
715
791
|
}
|
|
716
792
|
if (res.status === 429 && _retryCount < MAX_RETRIES) {
|
|
717
|
-
const
|
|
793
|
+
const MAX_RETRY_AFTER = 60;
|
|
794
|
+
const rawRetry = parseInt(res.headers.get("Retry-After") ?? "5", 10);
|
|
795
|
+
const retryAfter = Math.min(Number.isNaN(rawRetry) ? 5 : rawRetry, MAX_RETRY_AFTER);
|
|
718
796
|
await sleep(retryAfter * 1e3);
|
|
719
797
|
return request(opts, _retryCount + 1);
|
|
720
798
|
}
|
|
@@ -739,6 +817,58 @@ async function request(opts, _retryCount = 0) {
|
|
|
739
817
|
return JSON.parse(text);
|
|
740
818
|
}
|
|
741
819
|
|
|
820
|
+
// src/utils/sanitize.ts
|
|
821
|
+
import { basename, resolve, sep } from "path";
|
|
822
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
823
|
+
function sanitizePathSegment(value) {
|
|
824
|
+
if (!value || typeof value !== "string") {
|
|
825
|
+
throw new Error("Path segment must be a non-empty string");
|
|
826
|
+
}
|
|
827
|
+
if (value === "me") return value;
|
|
828
|
+
if (/[/\\]/.test(value) || value.includes("..")) {
|
|
829
|
+
throw new Error(`Invalid path segment: "${value}"`);
|
|
830
|
+
}
|
|
831
|
+
return encodeURIComponent(value);
|
|
832
|
+
}
|
|
833
|
+
function sanitizeFileName(name) {
|
|
834
|
+
if (!name || typeof name !== "string") {
|
|
835
|
+
throw new Error("File name must be a non-empty string");
|
|
836
|
+
}
|
|
837
|
+
const base = basename(name);
|
|
838
|
+
return base.replace(/[\r\n"\\]/g, "_");
|
|
839
|
+
}
|
|
840
|
+
function validateLocalPath(filePath, allowedBase) {
|
|
841
|
+
const resolved = resolve(filePath);
|
|
842
|
+
if (allowedBase) {
|
|
843
|
+
const resolvedBase = resolve(allowedBase);
|
|
844
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
|
|
845
|
+
throw new Error(`Path "${filePath}" escapes the allowed directory "${allowedBase}"`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return resolved;
|
|
849
|
+
}
|
|
850
|
+
function validateRedirectUrl(location, allowedHosts) {
|
|
851
|
+
let parsed;
|
|
852
|
+
try {
|
|
853
|
+
parsed = new URL(location);
|
|
854
|
+
} catch {
|
|
855
|
+
throw new Error(`Invalid redirect URL: "${location}"`);
|
|
856
|
+
}
|
|
857
|
+
if (parsed.protocol !== "https:") {
|
|
858
|
+
throw new Error(`Redirect URL must use HTTPS: "${location}"`);
|
|
859
|
+
}
|
|
860
|
+
const isAllowed = allowedHosts.some(
|
|
861
|
+
(host) => parsed.hostname === host || parsed.hostname.endsWith("." + host)
|
|
862
|
+
);
|
|
863
|
+
if (!isAllowed) {
|
|
864
|
+
throw new Error(`Redirect to untrusted host: "${parsed.hostname}"`);
|
|
865
|
+
}
|
|
866
|
+
return location;
|
|
867
|
+
}
|
|
868
|
+
function generateSecureState() {
|
|
869
|
+
return randomBytes3(32).toString("hex");
|
|
870
|
+
}
|
|
871
|
+
|
|
742
872
|
// src/api/message.ts
|
|
743
873
|
function buildContent(opts) {
|
|
744
874
|
const type = opts.type ?? "text";
|
|
@@ -746,7 +876,11 @@ function buildContent(opts) {
|
|
|
746
876
|
return { type: "text", text: opts.text };
|
|
747
877
|
}
|
|
748
878
|
if (type === "button") {
|
|
749
|
-
const actions = opts.actions ?
|
|
879
|
+
const actions = opts.actions ? (() => {
|
|
880
|
+
const parsed = JSON.parse(opts.actions);
|
|
881
|
+
if (!Array.isArray(parsed)) throw new Error("actions must be a JSON array");
|
|
882
|
+
return parsed;
|
|
883
|
+
})() : [];
|
|
750
884
|
return {
|
|
751
885
|
type: "button_template",
|
|
752
886
|
contentText: opts.text,
|
|
@@ -754,7 +888,11 @@ function buildContent(opts) {
|
|
|
754
888
|
};
|
|
755
889
|
}
|
|
756
890
|
if (type === "list") {
|
|
757
|
-
const elements = opts.elements ?
|
|
891
|
+
const elements = opts.elements ? (() => {
|
|
892
|
+
const parsed = JSON.parse(opts.elements);
|
|
893
|
+
if (!Array.isArray(parsed)) throw new Error("elements must be a JSON array");
|
|
894
|
+
return parsed;
|
|
895
|
+
})() : [];
|
|
758
896
|
return {
|
|
759
897
|
type: "list_template",
|
|
760
898
|
coverData: { text: opts.text },
|
|
@@ -776,7 +914,7 @@ async function send(opts) {
|
|
|
776
914
|
if (opts.to) {
|
|
777
915
|
const result = await request({
|
|
778
916
|
method: "POST",
|
|
779
|
-
path: `/bots/${creds.botId}/users/${opts.to}/messages`,
|
|
917
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/users/${sanitizePathSegment(opts.to)}/messages`,
|
|
780
918
|
body,
|
|
781
919
|
profile
|
|
782
920
|
});
|
|
@@ -785,7 +923,7 @@ async function send(opts) {
|
|
|
785
923
|
if (opts.channel) {
|
|
786
924
|
const result = await request({
|
|
787
925
|
method: "POST",
|
|
788
|
-
path: `/bots/${creds.botId}/channels/${opts.channel}/messages`,
|
|
926
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(opts.channel)}/messages`,
|
|
789
927
|
body,
|
|
790
928
|
profile
|
|
791
929
|
});
|
|
@@ -802,7 +940,7 @@ async function listMembers(channelId, profile = "default") {
|
|
|
802
940
|
}
|
|
803
941
|
const result = await request({
|
|
804
942
|
method: "GET",
|
|
805
|
-
path: `/bots/${creds.botId}/channels/${channelId}/members`,
|
|
943
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(channelId)}/members`,
|
|
806
944
|
profile
|
|
807
945
|
});
|
|
808
946
|
return { members: result.members ?? [], responseMetaData: result.responseMetaData };
|
|
@@ -936,7 +1074,7 @@ function normalizeDateTime(dt) {
|
|
|
936
1074
|
async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
|
|
937
1075
|
const from = encodeURIComponent(fromDateTime);
|
|
938
1076
|
const until = encodeURIComponent(untilDateTime);
|
|
939
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
1077
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
940
1078
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
941
1079
|
console.error(`[nworks] GET ${url}`);
|
|
942
1080
|
}
|
|
@@ -973,10 +1111,10 @@ async function createEvent(opts) {
|
|
|
973
1111
|
eventComponents: [eventComponent],
|
|
974
1112
|
sendNotification: opts.sendNotification ?? false
|
|
975
1113
|
};
|
|
976
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events`;
|
|
1114
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events`;
|
|
977
1115
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
978
1116
|
console.error(`[nworks] POST ${url}`);
|
|
979
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1117
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
980
1118
|
}
|
|
981
1119
|
const res = await authedFetch(
|
|
982
1120
|
url,
|
|
@@ -994,7 +1132,7 @@ async function createEvent(opts) {
|
|
|
994
1132
|
return await res.json();
|
|
995
1133
|
}
|
|
996
1134
|
async function getEvent(eventId, userId = "me", profile = "default") {
|
|
997
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}`;
|
|
1135
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}`;
|
|
998
1136
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
999
1137
|
console.error(`[nworks] GET ${url}`);
|
|
1000
1138
|
}
|
|
@@ -1037,10 +1175,10 @@ async function updateEvent(opts) {
|
|
|
1037
1175
|
eventComponents: [eventComponent],
|
|
1038
1176
|
sendNotification: opts.sendNotification ?? false
|
|
1039
1177
|
};
|
|
1040
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${opts.eventId}`;
|
|
1178
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(opts.eventId)}`;
|
|
1041
1179
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1042
1180
|
console.error(`[nworks] PUT ${url}`);
|
|
1043
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1181
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1044
1182
|
}
|
|
1045
1183
|
const res = await authedFetch(
|
|
1046
1184
|
url,
|
|
@@ -1056,7 +1194,7 @@ async function updateEvent(opts) {
|
|
|
1056
1194
|
async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
|
|
1057
1195
|
const params = new URLSearchParams();
|
|
1058
1196
|
params.set("sendNotification", String(sendNotification));
|
|
1059
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}?${params.toString()}`;
|
|
1197
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}?${params.toString()}`;
|
|
1060
1198
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1061
1199
|
console.error(`[nworks] DELETE ${url}`);
|
|
1062
1200
|
}
|
|
@@ -1182,9 +1320,15 @@ import { join as join2 } from "path";
|
|
|
1182
1320
|
import { Command as Command7 } from "commander";
|
|
1183
1321
|
|
|
1184
1322
|
// src/api/drive.ts
|
|
1185
|
-
import { readFile as readFile4, stat } from "fs/promises";
|
|
1186
|
-
import { basename } from "path";
|
|
1323
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1324
|
+
import { basename as basename2 } from "path";
|
|
1187
1325
|
var BASE_URL3 = "https://www.worksapis.com/v1.0";
|
|
1326
|
+
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
1327
|
+
var ALLOWED_HOSTS = [
|
|
1328
|
+
"storage.worksmobile.com",
|
|
1329
|
+
"www.worksapis.com",
|
|
1330
|
+
"worksapis.com"
|
|
1331
|
+
];
|
|
1188
1332
|
async function authedFetch2(url, init, profile) {
|
|
1189
1333
|
const token = await getValidUserToken(profile);
|
|
1190
1334
|
const headers = new Headers(init.headers);
|
|
@@ -1206,8 +1350,8 @@ async function handleError2(res) {
|
|
|
1206
1350
|
throw new ApiError(code, description, res.status);
|
|
1207
1351
|
}
|
|
1208
1352
|
async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
|
|
1209
|
-
const base = `${BASE_URL3}/users/${userId}/drive/files`;
|
|
1210
|
-
const path = folderId ? `${base}/${folderId}/children` : base;
|
|
1353
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1354
|
+
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
1211
1355
|
const params = new URLSearchParams();
|
|
1212
1356
|
params.set("count", String(count));
|
|
1213
1357
|
if (cursor) params.set("cursor", cursor);
|
|
@@ -1221,11 +1365,15 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
|
|
|
1221
1365
|
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1222
1366
|
}
|
|
1223
1367
|
async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1224
|
-
const fileName =
|
|
1225
|
-
const
|
|
1368
|
+
const fileName = basename2(localPath);
|
|
1369
|
+
const safeName = sanitizeFileName(fileName);
|
|
1370
|
+
const fileStat = await stat2(localPath);
|
|
1226
1371
|
const fileSize = fileStat.size;
|
|
1227
|
-
|
|
1228
|
-
|
|
1372
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
1373
|
+
throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
|
|
1374
|
+
}
|
|
1375
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1376
|
+
const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
|
|
1229
1377
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1230
1378
|
console.error(`[nworks] POST ${createUrl} (create upload URL)`);
|
|
1231
1379
|
}
|
|
@@ -1240,11 +1388,12 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1240
1388
|
);
|
|
1241
1389
|
if (!createRes.ok) return handleError2(createRes);
|
|
1242
1390
|
const { uploadUrl } = await createRes.json();
|
|
1391
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1243
1392
|
const fileBuffer = await readFile4(localPath);
|
|
1244
1393
|
const boundary = `----nworks${Date.now()}`;
|
|
1245
1394
|
const header = Buffer.from(
|
|
1246
1395
|
`--${boundary}\r
|
|
1247
|
-
Content-Disposition: form-data; name="Filedata"; filename="${
|
|
1396
|
+
Content-Disposition: form-data; name="Filedata"; filename="${safeName}"\r
|
|
1248
1397
|
Content-Type: application/octet-stream\r
|
|
1249
1398
|
\r
|
|
1250
1399
|
`
|
|
@@ -1270,8 +1419,11 @@ Content-Type: application/octet-stream\r
|
|
|
1270
1419
|
}
|
|
1271
1420
|
async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1272
1421
|
const fileSize = fileBuffer.length;
|
|
1273
|
-
|
|
1274
|
-
|
|
1422
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
1423
|
+
throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
|
|
1424
|
+
}
|
|
1425
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1426
|
+
const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
|
|
1275
1427
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1276
1428
|
console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
|
|
1277
1429
|
}
|
|
@@ -1286,10 +1438,11 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1286
1438
|
);
|
|
1287
1439
|
if (!createRes.ok) return handleError2(createRes);
|
|
1288
1440
|
const { uploadUrl } = await createRes.json();
|
|
1441
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1289
1442
|
const boundary = `----nworks${Date.now()}`;
|
|
1290
1443
|
const header = Buffer.from(
|
|
1291
1444
|
`--${boundary}\r
|
|
1292
|
-
Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
|
|
1445
|
+
Content-Disposition: form-data; name="Filedata"; filename="${sanitizeFileName(fileName)}"\r
|
|
1293
1446
|
Content-Type: application/octet-stream\r
|
|
1294
1447
|
\r
|
|
1295
1448
|
`
|
|
@@ -1314,7 +1467,7 @@ Content-Type: application/octet-stream\r
|
|
|
1314
1467
|
return await uploadRes.json();
|
|
1315
1468
|
}
|
|
1316
1469
|
async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
1317
|
-
const url = `${BASE_URL3}/users/${userId}/drive/files/${fileId}/download`;
|
|
1470
|
+
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
1318
1471
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1319
1472
|
console.error(`[nworks] GET ${url} (get download URL)`);
|
|
1320
1473
|
}
|
|
@@ -1331,10 +1484,11 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
|
1331
1484
|
if (!redirectRes.ok) return handleError2(redirectRes);
|
|
1332
1485
|
throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
|
|
1333
1486
|
}
|
|
1487
|
+
const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
|
|
1334
1488
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1335
|
-
console.error(`[nworks] GET ${
|
|
1489
|
+
console.error(`[nworks] GET ${safeLocation} (download content)`);
|
|
1336
1490
|
}
|
|
1337
|
-
const downloadRes = await
|
|
1491
|
+
const downloadRes = await fetch(safeLocation, { method: "GET" });
|
|
1338
1492
|
if (!downloadRes.ok) return handleError2(downloadRes);
|
|
1339
1493
|
const arrayBuffer = await downloadRes.arrayBuffer();
|
|
1340
1494
|
const buffer = Buffer.from(arrayBuffer);
|
|
@@ -1471,7 +1625,7 @@ async function handleError3(res) {
|
|
|
1471
1625
|
async function sendMail(opts) {
|
|
1472
1626
|
const userId = opts.userId ?? "me";
|
|
1473
1627
|
const profile = opts.profile ?? "default";
|
|
1474
|
-
const url = `${BASE_URL4}/users/${userId}/mail`;
|
|
1628
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail`;
|
|
1475
1629
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1476
1630
|
console.error(`[nworks] POST ${url}`);
|
|
1477
1631
|
}
|
|
@@ -1500,7 +1654,7 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1500
1654
|
params.set("count", String(count));
|
|
1501
1655
|
if (cursor) params.set("cursor", cursor);
|
|
1502
1656
|
if (isUnread) params.set("isUnread", "true");
|
|
1503
|
-
const url = `${BASE_URL4}/users/${userId}/mail/mailfolders/${folderId}/children?${params.toString()}`;
|
|
1657
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/mailfolders/${sanitizePathSegment(String(folderId))}/children?${params.toString()}`;
|
|
1504
1658
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1505
1659
|
console.error(`[nworks] GET ${url}`);
|
|
1506
1660
|
}
|
|
@@ -1516,7 +1670,7 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1516
1670
|
};
|
|
1517
1671
|
}
|
|
1518
1672
|
async function readMail(mailId, userId = "me", profile = "default") {
|
|
1519
|
-
const url = `${BASE_URL4}/users/${userId}/mail/${mailId}`;
|
|
1673
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}`;
|
|
1520
1674
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1521
1675
|
console.error(`[nworks] GET ${url}`);
|
|
1522
1676
|
}
|
|
@@ -1640,7 +1794,7 @@ async function handleError4(res) {
|
|
|
1640
1794
|
}
|
|
1641
1795
|
async function resolveUserId(userId, profile) {
|
|
1642
1796
|
if (userId !== "me") return userId;
|
|
1643
|
-
const url = `${BASE_URL5}/users
|
|
1797
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
|
|
1644
1798
|
const res = await authedFetch4(url, { method: "GET" }, profile);
|
|
1645
1799
|
if (!res.ok) return handleError4(res);
|
|
1646
1800
|
const data = await res.json();
|
|
@@ -1655,7 +1809,7 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
|
|
|
1655
1809
|
params.set("count", String(count));
|
|
1656
1810
|
params.set("status", status);
|
|
1657
1811
|
if (cursor) params.set("cursor", cursor);
|
|
1658
|
-
const url = `${BASE_URL5}/users/${userId}/tasks?${params.toString()}`;
|
|
1812
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks?${params.toString()}`;
|
|
1659
1813
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1660
1814
|
console.error(`[nworks] GET ${url}`);
|
|
1661
1815
|
}
|
|
@@ -1679,10 +1833,10 @@ async function createTask(opts) {
|
|
|
1679
1833
|
};
|
|
1680
1834
|
if (opts.dueDate) body.dueDate = opts.dueDate;
|
|
1681
1835
|
if (opts.categoryId) body.categoryId = opts.categoryId;
|
|
1682
|
-
const url = `${BASE_URL5}/users/${userId}/tasks`;
|
|
1836
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks`;
|
|
1683
1837
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1684
1838
|
console.error(`[nworks] POST ${url}`);
|
|
1685
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1839
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1686
1840
|
}
|
|
1687
1841
|
const res = await authedFetch4(
|
|
1688
1842
|
url,
|
|
@@ -1705,7 +1859,7 @@ async function updateTask(opts) {
|
|
|
1705
1859
|
if (opts.title !== void 0) body.title = opts.title;
|
|
1706
1860
|
if (opts.content !== void 0) body.content = opts.content;
|
|
1707
1861
|
if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
|
|
1708
|
-
const url = `${BASE_URL5}/tasks/${opts.taskId}`;
|
|
1862
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(opts.taskId)}`;
|
|
1709
1863
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1710
1864
|
console.error(`[nworks] PATCH ${url}`);
|
|
1711
1865
|
}
|
|
@@ -1722,7 +1876,7 @@ async function updateTask(opts) {
|
|
|
1722
1876
|
return await res.json();
|
|
1723
1877
|
}
|
|
1724
1878
|
async function completeTask(taskId, profile = "default") {
|
|
1725
|
-
const url = `${BASE_URL5}/tasks/${taskId}/complete`;
|
|
1879
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/complete`;
|
|
1726
1880
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1727
1881
|
console.error(`[nworks] POST ${url}`);
|
|
1728
1882
|
}
|
|
@@ -1735,7 +1889,7 @@ async function completeTask(taskId, profile = "default") {
|
|
|
1735
1889
|
if (!res.ok) return handleError4(res);
|
|
1736
1890
|
}
|
|
1737
1891
|
async function incompleteTask(taskId, profile = "default") {
|
|
1738
|
-
const url = `${BASE_URL5}/tasks/${taskId}/incomplete`;
|
|
1892
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
|
|
1739
1893
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1740
1894
|
console.error(`[nworks] POST ${url}`);
|
|
1741
1895
|
}
|
|
@@ -1748,7 +1902,7 @@ async function incompleteTask(taskId, profile = "default") {
|
|
|
1748
1902
|
if (!res.ok) return handleError4(res);
|
|
1749
1903
|
}
|
|
1750
1904
|
async function deleteTask(taskId, profile = "default") {
|
|
1751
|
-
const url = `${BASE_URL5}/tasks/${taskId}`;
|
|
1905
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
|
|
1752
1906
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1753
1907
|
console.error(`[nworks] DELETE ${url}`);
|
|
1754
1908
|
}
|
|
@@ -1920,7 +2074,7 @@ async function listBoards(count = 20, cursor, profile = "default") {
|
|
|
1920
2074
|
if (!res.ok) return handleError5(res);
|
|
1921
2075
|
const text = await res.text();
|
|
1922
2076
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1923
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2077
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1924
2078
|
}
|
|
1925
2079
|
const data = safeParseJson(text);
|
|
1926
2080
|
return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
|
|
@@ -1929,7 +2083,7 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1929
2083
|
const params = new URLSearchParams();
|
|
1930
2084
|
params.set("count", String(count));
|
|
1931
2085
|
if (cursor) params.set("cursor", cursor);
|
|
1932
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts?${params.toString()}`;
|
|
2086
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts?${params.toString()}`;
|
|
1933
2087
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1934
2088
|
console.error(`[nworks] GET ${url}`);
|
|
1935
2089
|
}
|
|
@@ -1937,13 +2091,13 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1937
2091
|
if (!res.ok) return handleError5(res);
|
|
1938
2092
|
const text = await res.text();
|
|
1939
2093
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1940
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2094
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1941
2095
|
}
|
|
1942
2096
|
const data = safeParseJson(text);
|
|
1943
2097
|
return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
|
|
1944
2098
|
}
|
|
1945
2099
|
async function readPost(boardId, postId, profile = "default") {
|
|
1946
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts/${postId}`;
|
|
2100
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts/${sanitizePathSegment(postId)}`;
|
|
1947
2101
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1948
2102
|
console.error(`[nworks] GET ${url}`);
|
|
1949
2103
|
}
|
|
@@ -1951,7 +2105,7 @@ async function readPost(boardId, postId, profile = "default") {
|
|
|
1951
2105
|
if (!res.ok) return handleError5(res);
|
|
1952
2106
|
const text = await res.text();
|
|
1953
2107
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1954
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2108
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1955
2109
|
}
|
|
1956
2110
|
return safeParseJson(text);
|
|
1957
2111
|
}
|
|
@@ -1963,10 +2117,10 @@ async function createPost(opts) {
|
|
|
1963
2117
|
};
|
|
1964
2118
|
if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
|
|
1965
2119
|
if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
|
|
1966
|
-
const url = `${BASE_URL6}/boards/${opts.boardId}/posts`;
|
|
2120
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(opts.boardId)}/posts`;
|
|
1967
2121
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1968
2122
|
console.error(`[nworks] POST ${url}`);
|
|
1969
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
2123
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1970
2124
|
}
|
|
1971
2125
|
const res = await authedFetch5(
|
|
1972
2126
|
url,
|
|
@@ -1980,7 +2134,7 @@ async function createPost(opts) {
|
|
|
1980
2134
|
if (res.status === 201 || res.ok) {
|
|
1981
2135
|
const text = await res.text();
|
|
1982
2136
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1983
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2137
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1984
2138
|
}
|
|
1985
2139
|
return safeParseJson(text);
|
|
1986
2140
|
}
|
|
@@ -2103,7 +2257,7 @@ async function runChecks(profile) {
|
|
|
2103
2257
|
creds = await loadCredentials(profile);
|
|
2104
2258
|
results.push({ check: "credentials", status: "OK", detail: `clientId: ${creds.clientId}` });
|
|
2105
2259
|
} catch {
|
|
2106
|
-
results.push({ check: "credentials", status: "FAIL", detail: "\uC778\uC99D \uC815\uBCF4 \uC5C6\uC74C. `nworks login` \
|
|
2260
|
+
results.push({ check: "credentials", status: "FAIL", detail: "\uC778\uC99D \uC815\uBCF4 \uC5C6\uC74C. CLI: `nworks login --user` / MCP: nworks_setup tool \uC0AC\uC6A9 (\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET \uD544\uC694)" });
|
|
2107
2261
|
return results;
|
|
2108
2262
|
}
|
|
2109
2263
|
if (hasServiceAccountCreds(creds)) {
|
|
@@ -2205,7 +2359,26 @@ var doctorCommand = new Command11("doctor").description("Check nworks configurat
|
|
|
2205
2359
|
function registerTools(server) {
|
|
2206
2360
|
server.tool(
|
|
2207
2361
|
"nworks_setup",
|
|
2208
|
-
|
|
2362
|
+
`NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
|
|
2363
|
+
|
|
2364
|
+
\u25A0 \uC0AC\uC804 \uC900\uBE44 (\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uD574\uC57C \uD568):
|
|
2365
|
+
1. https://dev.worksmobile.com \uC5D0\uC11C \uC571 \uC0DD\uC131 \uD6C4 Client ID\uC640 Client Secret\uC744 \uBC1C\uAE09\uBC1B\uC2B5\uB2C8\uB2E4.
|
|
2366
|
+
2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC758 nworks \uC11C\uBC84\uC5D0 env \uD544\uB4DC\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4:
|
|
2367
|
+
{ "env": { "NWORKS_CLIENT_SECRET": "<\uBC1C\uAE09\uBC1B\uC740 Client Secret>" } }
|
|
2368
|
+
3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.
|
|
2369
|
+
|
|
2370
|
+
\u25A0 \uC774 tool\uC758 \uC5ED\uD560:
|
|
2371
|
+
- clientId(\uD544\uC218)\uC640 serviceAccount, botId, domainId(\uC120\uD0DD)\uB97C \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC544 \uC800\uC7A5\uD569\uB2C8\uB2E4.
|
|
2372
|
+
- Client Secret\uC740 \uBCF4\uC548\uC744 \uC704\uD574 \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC9C0 \uC54A\uC73C\uBA70, \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC77D\uC2B5\uB2C8\uB2E4.
|
|
2373
|
+
- Service Account \uC0AC\uC6A9 \uC2DC \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH\uB3C4 \uD544\uC694\uD569\uB2C8\uB2E4.
|
|
2374
|
+
|
|
2375
|
+
\u25A0 \uC124\uC815 \uD6C4 \uB2E4\uC74C \uB2E8\uACC4:
|
|
2376
|
+
- \uCE98\uB9B0\uB354/\uBA54\uC77C/\uB4DC\uB77C\uC774\uBE0C/\uD560\uC77C/\uAC8C\uC2DC\uD310 \u2192 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778 \uD544\uC694
|
|
2377
|
+
- \uBA54\uC2DC\uC9C0/\uAD6C\uC131\uC6D0\uC870\uD68C \u2192 Service Account \uC778\uC99D (serviceAccount + botId + NWORKS_PRIVATE_KEY_PATH)
|
|
2378
|
+
|
|
2379
|
+
\u25A0 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC5C6\uC73C\uBA74 \uC774 tool\uC740 \uC2E4\uD328\uD569\uB2C8\uB2E4. \uC2E4\uD328 \uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC704 \uC0AC\uC804 \uC900\uBE44 \uB2E8\uACC4\uB97C \uC548\uB0B4\uD558\uC138\uC694.
|
|
2380
|
+
|
|
2381
|
+
OAuth Redirect URI: http://localhost:9876/callback`,
|
|
2209
2382
|
{
|
|
2210
2383
|
clientId: z.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
|
|
2211
2384
|
serviceAccount: z.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
|
|
@@ -2220,19 +2393,14 @@ function registerTools(server) {
|
|
|
2220
2393
|
content: [{ type: "text", text: JSON.stringify({
|
|
2221
2394
|
error: true,
|
|
2222
2395
|
message: "\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
},
|
|
2235
|
-
developerConsole: "https://dev.worksmobile.com"
|
|
2396
|
+
userAction: [
|
|
2397
|
+
"1. https://dev.worksmobile.com \uC5D0\uC11C \uC571\uC758 Client Secret\uC744 \uD655\uC778\uD569\uB2C8\uB2E4.",
|
|
2398
|
+
"2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC744 \uC5F4\uACE0, nworks \uC11C\uBC84 \uC124\uC815\uC5D0 \uB2E4\uC74C\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4:",
|
|
2399
|
+
' "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" }',
|
|
2400
|
+
"3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.",
|
|
2401
|
+
"4. \uC7AC\uC2DC\uC791 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
|
|
2402
|
+
],
|
|
2403
|
+
configExample: '{ "mcpServers": { "nworks": { "command": "npx", "args": ["-y", "nworks", "mcp"], "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" } } } }'
|
|
2236
2404
|
}) }],
|
|
2237
2405
|
isError: true
|
|
2238
2406
|
};
|
|
@@ -2250,7 +2418,7 @@ function registerTools(server) {
|
|
|
2250
2418
|
if (serviceAccount && resolvedPrivateKeyPath && botId) {
|
|
2251
2419
|
nextSteps.push("Service Account \uC778\uC99D \uC900\uBE44 \uC644\uB8CC \u2014 \uBD07 \uBA54\uC2DC\uC9C0 \uB4F1 \uBC14\uB85C \uC0AC\uC6A9 \uAC00\uB2A5");
|
|
2252
2420
|
} else if (serviceAccount && botId && !resolvedPrivateKeyPath) {
|
|
2253
|
-
nextSteps.push(
|
|
2421
|
+
nextSteps.push('NWORKS_PRIVATE_KEY_PATH \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. Service Account \uC778\uC99D\uC5D0\uB294 Private Key \uD30C\uC77C \uACBD\uB85C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC548\uB0B4\uD558\uC138\uC694: (1) Developer Console(https://dev.worksmobile.com)\uC5D0\uC11C Private Key\uB97C \uB2E4\uC6B4\uB85C\uB4DC (2) MCP \uC124\uC815 \uD30C\uC77C\uC758 env\uC5D0 NWORKS_PRIVATE_KEY_PATH\uB97C \uCD94\uAC00 (\uC608: "NWORKS_PRIVATE_KEY_PATH": "C:/keys/private.key") (3) MCP \uD074\uB77C\uC774\uC5B8\uD2B8 \uC7AC\uC2DC\uC791');
|
|
2254
2422
|
}
|
|
2255
2423
|
nextSteps.push("User OAuth\uAC00 \uD544\uC694\uD55C API\uB294 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC744 \uC9C4\uD589\uD558\uC138\uC694");
|
|
2256
2424
|
const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
|
|
@@ -2262,7 +2430,7 @@ function registerTools(server) {
|
|
|
2262
2430
|
success: true,
|
|
2263
2431
|
message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
2264
2432
|
nextSteps,
|
|
2265
|
-
clientId,
|
|
2433
|
+
clientId: mask(clientId),
|
|
2266
2434
|
clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
|
|
2267
2435
|
serviceAccount: serviceAccount ?? null,
|
|
2268
2436
|
privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
|
|
@@ -2398,7 +2566,17 @@ function registerTools(server) {
|
|
|
2398
2566
|
sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
2399
2567
|
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2400
2568
|
},
|
|
2401
|
-
async ({
|
|
2569
|
+
async ({
|
|
2570
|
+
summary,
|
|
2571
|
+
start,
|
|
2572
|
+
end,
|
|
2573
|
+
timeZone,
|
|
2574
|
+
description,
|
|
2575
|
+
location,
|
|
2576
|
+
attendees,
|
|
2577
|
+
sendNotification,
|
|
2578
|
+
userId
|
|
2579
|
+
}) => {
|
|
2402
2580
|
try {
|
|
2403
2581
|
const result = await createEvent({
|
|
2404
2582
|
summary,
|
|
@@ -2437,7 +2615,17 @@ function registerTools(server) {
|
|
|
2437
2615
|
sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
2438
2616
|
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2439
2617
|
},
|
|
2440
|
-
async ({
|
|
2618
|
+
async ({
|
|
2619
|
+
eventId,
|
|
2620
|
+
summary,
|
|
2621
|
+
start,
|
|
2622
|
+
end,
|
|
2623
|
+
timeZone,
|
|
2624
|
+
description,
|
|
2625
|
+
location,
|
|
2626
|
+
sendNotification,
|
|
2627
|
+
userId
|
|
2628
|
+
}) => {
|
|
2441
2629
|
try {
|
|
2442
2630
|
await updateEvent({
|
|
2443
2631
|
eventId,
|
|
@@ -2550,11 +2738,12 @@ function registerTools(server) {
|
|
|
2550
2738
|
overwrite ?? false
|
|
2551
2739
|
);
|
|
2552
2740
|
} else if (filePath) {
|
|
2741
|
+
const safePath = validateLocalPath(filePath);
|
|
2553
2742
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2554
|
-
console.error(`[nworks] MCP upload: filePath=${
|
|
2743
|
+
console.error(`[nworks] MCP upload: filePath=${safePath}`);
|
|
2555
2744
|
}
|
|
2556
2745
|
result = await uploadFile(
|
|
2557
|
-
|
|
2746
|
+
safePath,
|
|
2558
2747
|
userId ?? "me",
|
|
2559
2748
|
folderId,
|
|
2560
2749
|
overwrite ?? false
|
|
@@ -2569,10 +2758,11 @@ function registerTools(server) {
|
|
|
2569
2758
|
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
2570
2759
|
};
|
|
2571
2760
|
} catch (err) {
|
|
2572
|
-
|
|
2573
|
-
|
|
2761
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2762
|
+
console.error(`[nworks] drive upload error: ${err.stack}`);
|
|
2763
|
+
}
|
|
2574
2764
|
return {
|
|
2575
|
-
content: [{ type: "text", text:
|
|
2765
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
|
|
2576
2766
|
isError: true
|
|
2577
2767
|
};
|
|
2578
2768
|
}
|
|
@@ -2597,7 +2787,10 @@ function registerTools(server) {
|
|
|
2597
2787
|
if (outputDir) {
|
|
2598
2788
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
2599
2789
|
const { join: join3 } = await import("path");
|
|
2600
|
-
const
|
|
2790
|
+
const safeDir = validateLocalPath(outputDir);
|
|
2791
|
+
const safeName = sanitizeFileName(fileName);
|
|
2792
|
+
const outPath = join3(safeDir, safeName);
|
|
2793
|
+
validateLocalPath(outPath, safeDir);
|
|
2601
2794
|
await writeFile3(outPath, result.buffer);
|
|
2602
2795
|
return {
|
|
2603
2796
|
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
@@ -3018,9 +3211,9 @@ function registerTools(server) {
|
|
|
3018
3211
|
const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
3019
3212
|
const requestedScopes = expandScopes((scope ?? DEFAULT_SCOPE).split(" ").filter(Boolean));
|
|
3020
3213
|
const mergedScopes = [.../* @__PURE__ */ new Set([...existingScopes, ...requestedScopes])].join(" ");
|
|
3021
|
-
const state =
|
|
3214
|
+
const state = generateSecureState();
|
|
3022
3215
|
const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
|
|
3023
|
-
startOAuthCallbackServer(creds.clientId, creds.clientSecret).then(
|
|
3216
|
+
startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
|
|
3024
3217
|
(token) => saveUserToken({
|
|
3025
3218
|
accessToken: token.accessToken,
|
|
3026
3219
|
refreshToken: token.refreshToken,
|
|
@@ -3065,9 +3258,10 @@ function registerTools(server) {
|
|
|
3065
3258
|
const userToken = await loadUserToken();
|
|
3066
3259
|
const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
|
|
3067
3260
|
const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
|
|
3261
|
+
const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
3068
3262
|
const info = {
|
|
3069
3263
|
serviceAccount: creds.serviceAccount ?? null,
|
|
3070
|
-
clientId: creds.clientId,
|
|
3264
|
+
clientId: mask(creds.clientId),
|
|
3071
3265
|
botId: creds.botId ?? null,
|
|
3072
3266
|
tokenValid: isValid,
|
|
3073
3267
|
userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
|
|
@@ -3089,6 +3283,18 @@ function registerTools(server) {
|
|
|
3089
3283
|
{},
|
|
3090
3284
|
async () => {
|
|
3091
3285
|
try {
|
|
3286
|
+
try {
|
|
3287
|
+
const creds = await loadCredentials();
|
|
3288
|
+
const token = await loadToken();
|
|
3289
|
+
const userToken = await loadUserToken();
|
|
3290
|
+
if (token?.accessToken) {
|
|
3291
|
+
await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
|
|
3292
|
+
}
|
|
3293
|
+
if (userToken?.refreshToken) {
|
|
3294
|
+
await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
|
|
3295
|
+
}
|
|
3296
|
+
} catch {
|
|
3297
|
+
}
|
|
3092
3298
|
await clearCredentials();
|
|
3093
3299
|
return {
|
|
3094
3300
|
content: [
|
|
@@ -3096,7 +3302,7 @@ function registerTools(server) {
|
|
|
3096
3302
|
type: "text",
|
|
3097
3303
|
text: JSON.stringify({
|
|
3098
3304
|
success: true,
|
|
3099
|
-
message: "\uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC774 \uBAA8\uB450 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC0AC\uC6A9\uD558\uB824\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815\uD558\uC138\uC694."
|
|
3305
|
+
message: "\uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC774 \uBAA8\uB450 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC0AC\uC6A9\uD558\uB824\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815 \uD6C4 nworks_login_user\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uD558\uC138\uC694."
|
|
3100
3306
|
})
|
|
3101
3307
|
}
|
|
3102
3308
|
]
|
|
@@ -3116,8 +3322,21 @@ function registerTools(server) {
|
|
|
3116
3322
|
async () => {
|
|
3117
3323
|
try {
|
|
3118
3324
|
const results = await runChecks("default");
|
|
3325
|
+
const maskedResults = results.map((r) => {
|
|
3326
|
+
if (r.check === "credentials" && r.status === "OK") {
|
|
3327
|
+
return { ...r, detail: r.detail.replace(/clientId: .+/, "clientId: ****") };
|
|
3328
|
+
}
|
|
3329
|
+
if (r.check === "privateKey" && r.status === "OK") {
|
|
3330
|
+
return { ...r, detail: "OK (path hidden)" };
|
|
3331
|
+
}
|
|
3332
|
+
if (r.check === "serviceAccount" && r.status === "OK") {
|
|
3333
|
+
const masked = r.detail.length <= 4 ? "****" : `****${r.detail.slice(-4)}`;
|
|
3334
|
+
return { ...r, detail: masked };
|
|
3335
|
+
}
|
|
3336
|
+
return r;
|
|
3337
|
+
});
|
|
3119
3338
|
return {
|
|
3120
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3339
|
+
content: [{ type: "text", text: JSON.stringify(maskedResults) }]
|
|
3121
3340
|
};
|
|
3122
3341
|
} catch (err) {
|
|
3123
3342
|
return {
|