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/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...`);
|
|
@@ -475,7 +535,7 @@ Opening browser for NAVER WORKS login...`);
|
|
|
475
535
|
} else {
|
|
476
536
|
exec(`${openCmd} "${authorizeUrl}"`);
|
|
477
537
|
}
|
|
478
|
-
const tokenData = await startUserOAuthFlow(scope, profile);
|
|
538
|
+
const tokenData = await startUserOAuthFlow(scope, profile, state);
|
|
479
539
|
await saveUserToken(tokenData, profile);
|
|
480
540
|
output(
|
|
481
541
|
{
|
|
@@ -652,6 +712,18 @@ function cliError(err, opts = {}, area) {
|
|
|
652
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) => {
|
|
653
713
|
try {
|
|
654
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
|
+
}
|
|
655
727
|
await clearCredentials(profile);
|
|
656
728
|
output({ success: true, message: `Logged out (profile: ${profile})` }, opts);
|
|
657
729
|
} catch (err) {
|
|
@@ -696,7 +768,7 @@ import { Command as Command4 } from "commander";
|
|
|
696
768
|
var BASE_URL = "https://www.worksapis.com/v1.0";
|
|
697
769
|
var MAX_RETRIES = 3;
|
|
698
770
|
function sleep(ms) {
|
|
699
|
-
return new Promise((
|
|
771
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
700
772
|
}
|
|
701
773
|
async function request(opts, _retryCount = 0) {
|
|
702
774
|
const { method, path, body, profile = "default" } = opts;
|
|
@@ -718,7 +790,9 @@ async function request(opts, _retryCount = 0) {
|
|
|
718
790
|
return request(opts, _retryCount + 1);
|
|
719
791
|
}
|
|
720
792
|
if (res.status === 429 && _retryCount < MAX_RETRIES) {
|
|
721
|
-
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);
|
|
722
796
|
await sleep(retryAfter * 1e3);
|
|
723
797
|
return request(opts, _retryCount + 1);
|
|
724
798
|
}
|
|
@@ -743,6 +817,58 @@ async function request(opts, _retryCount = 0) {
|
|
|
743
817
|
return JSON.parse(text);
|
|
744
818
|
}
|
|
745
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
|
+
|
|
746
872
|
// src/api/message.ts
|
|
747
873
|
function buildContent(opts) {
|
|
748
874
|
const type = opts.type ?? "text";
|
|
@@ -750,7 +876,11 @@ function buildContent(opts) {
|
|
|
750
876
|
return { type: "text", text: opts.text };
|
|
751
877
|
}
|
|
752
878
|
if (type === "button") {
|
|
753
|
-
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
|
+
})() : [];
|
|
754
884
|
return {
|
|
755
885
|
type: "button_template",
|
|
756
886
|
contentText: opts.text,
|
|
@@ -758,7 +888,11 @@ function buildContent(opts) {
|
|
|
758
888
|
};
|
|
759
889
|
}
|
|
760
890
|
if (type === "list") {
|
|
761
|
-
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
|
+
})() : [];
|
|
762
896
|
return {
|
|
763
897
|
type: "list_template",
|
|
764
898
|
coverData: { text: opts.text },
|
|
@@ -780,7 +914,7 @@ async function send(opts) {
|
|
|
780
914
|
if (opts.to) {
|
|
781
915
|
const result = await request({
|
|
782
916
|
method: "POST",
|
|
783
|
-
path: `/bots/${creds.botId}/users/${opts.to}/messages`,
|
|
917
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/users/${sanitizePathSegment(opts.to)}/messages`,
|
|
784
918
|
body,
|
|
785
919
|
profile
|
|
786
920
|
});
|
|
@@ -789,7 +923,7 @@ async function send(opts) {
|
|
|
789
923
|
if (opts.channel) {
|
|
790
924
|
const result = await request({
|
|
791
925
|
method: "POST",
|
|
792
|
-
path: `/bots/${creds.botId}/channels/${opts.channel}/messages`,
|
|
926
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(opts.channel)}/messages`,
|
|
793
927
|
body,
|
|
794
928
|
profile
|
|
795
929
|
});
|
|
@@ -806,7 +940,7 @@ async function listMembers(channelId, profile = "default") {
|
|
|
806
940
|
}
|
|
807
941
|
const result = await request({
|
|
808
942
|
method: "GET",
|
|
809
|
-
path: `/bots/${creds.botId}/channels/${channelId}/members`,
|
|
943
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(channelId)}/members`,
|
|
810
944
|
profile
|
|
811
945
|
});
|
|
812
946
|
return { members: result.members ?? [], responseMetaData: result.responseMetaData };
|
|
@@ -940,7 +1074,7 @@ function normalizeDateTime(dt) {
|
|
|
940
1074
|
async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
|
|
941
1075
|
const from = encodeURIComponent(fromDateTime);
|
|
942
1076
|
const until = encodeURIComponent(untilDateTime);
|
|
943
|
-
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}`;
|
|
944
1078
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
945
1079
|
console.error(`[nworks] GET ${url}`);
|
|
946
1080
|
}
|
|
@@ -977,10 +1111,10 @@ async function createEvent(opts) {
|
|
|
977
1111
|
eventComponents: [eventComponent],
|
|
978
1112
|
sendNotification: opts.sendNotification ?? false
|
|
979
1113
|
};
|
|
980
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events`;
|
|
1114
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events`;
|
|
981
1115
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
982
1116
|
console.error(`[nworks] POST ${url}`);
|
|
983
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1117
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
984
1118
|
}
|
|
985
1119
|
const res = await authedFetch(
|
|
986
1120
|
url,
|
|
@@ -998,7 +1132,7 @@ async function createEvent(opts) {
|
|
|
998
1132
|
return await res.json();
|
|
999
1133
|
}
|
|
1000
1134
|
async function getEvent(eventId, userId = "me", profile = "default") {
|
|
1001
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}`;
|
|
1135
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}`;
|
|
1002
1136
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1003
1137
|
console.error(`[nworks] GET ${url}`);
|
|
1004
1138
|
}
|
|
@@ -1041,10 +1175,10 @@ async function updateEvent(opts) {
|
|
|
1041
1175
|
eventComponents: [eventComponent],
|
|
1042
1176
|
sendNotification: opts.sendNotification ?? false
|
|
1043
1177
|
};
|
|
1044
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${opts.eventId}`;
|
|
1178
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(opts.eventId)}`;
|
|
1045
1179
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1046
1180
|
console.error(`[nworks] PUT ${url}`);
|
|
1047
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1181
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1048
1182
|
}
|
|
1049
1183
|
const res = await authedFetch(
|
|
1050
1184
|
url,
|
|
@@ -1060,7 +1194,7 @@ async function updateEvent(opts) {
|
|
|
1060
1194
|
async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
|
|
1061
1195
|
const params = new URLSearchParams();
|
|
1062
1196
|
params.set("sendNotification", String(sendNotification));
|
|
1063
|
-
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()}`;
|
|
1064
1198
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1065
1199
|
console.error(`[nworks] DELETE ${url}`);
|
|
1066
1200
|
}
|
|
@@ -1186,9 +1320,15 @@ import { join as join2 } from "path";
|
|
|
1186
1320
|
import { Command as Command7 } from "commander";
|
|
1187
1321
|
|
|
1188
1322
|
// src/api/drive.ts
|
|
1189
|
-
import { readFile as readFile4, stat } from "fs/promises";
|
|
1190
|
-
import { basename } from "path";
|
|
1323
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1324
|
+
import { basename as basename2 } from "path";
|
|
1191
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
|
+
];
|
|
1192
1332
|
async function authedFetch2(url, init, profile) {
|
|
1193
1333
|
const token = await getValidUserToken(profile);
|
|
1194
1334
|
const headers = new Headers(init.headers);
|
|
@@ -1210,8 +1350,8 @@ async function handleError2(res) {
|
|
|
1210
1350
|
throw new ApiError(code, description, res.status);
|
|
1211
1351
|
}
|
|
1212
1352
|
async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
|
|
1213
|
-
const base = `${BASE_URL3}/users/${userId}/drive/files`;
|
|
1214
|
-
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;
|
|
1215
1355
|
const params = new URLSearchParams();
|
|
1216
1356
|
params.set("count", String(count));
|
|
1217
1357
|
if (cursor) params.set("cursor", cursor);
|
|
@@ -1225,11 +1365,15 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
|
|
|
1225
1365
|
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1226
1366
|
}
|
|
1227
1367
|
async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1228
|
-
const fileName =
|
|
1229
|
-
const
|
|
1368
|
+
const fileName = basename2(localPath);
|
|
1369
|
+
const safeName = sanitizeFileName(fileName);
|
|
1370
|
+
const fileStat = await stat2(localPath);
|
|
1230
1371
|
const fileSize = fileStat.size;
|
|
1231
|
-
|
|
1232
|
-
|
|
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;
|
|
1233
1377
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1234
1378
|
console.error(`[nworks] POST ${createUrl} (create upload URL)`);
|
|
1235
1379
|
}
|
|
@@ -1244,11 +1388,12 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1244
1388
|
);
|
|
1245
1389
|
if (!createRes.ok) return handleError2(createRes);
|
|
1246
1390
|
const { uploadUrl } = await createRes.json();
|
|
1391
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1247
1392
|
const fileBuffer = await readFile4(localPath);
|
|
1248
1393
|
const boundary = `----nworks${Date.now()}`;
|
|
1249
1394
|
const header = Buffer.from(
|
|
1250
1395
|
`--${boundary}\r
|
|
1251
|
-
Content-Disposition: form-data; name="Filedata"; filename="${
|
|
1396
|
+
Content-Disposition: form-data; name="Filedata"; filename="${safeName}"\r
|
|
1252
1397
|
Content-Type: application/octet-stream\r
|
|
1253
1398
|
\r
|
|
1254
1399
|
`
|
|
@@ -1274,8 +1419,11 @@ Content-Type: application/octet-stream\r
|
|
|
1274
1419
|
}
|
|
1275
1420
|
async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1276
1421
|
const fileSize = fileBuffer.length;
|
|
1277
|
-
|
|
1278
|
-
|
|
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;
|
|
1279
1427
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1280
1428
|
console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
|
|
1281
1429
|
}
|
|
@@ -1290,10 +1438,11 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1290
1438
|
);
|
|
1291
1439
|
if (!createRes.ok) return handleError2(createRes);
|
|
1292
1440
|
const { uploadUrl } = await createRes.json();
|
|
1441
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1293
1442
|
const boundary = `----nworks${Date.now()}`;
|
|
1294
1443
|
const header = Buffer.from(
|
|
1295
1444
|
`--${boundary}\r
|
|
1296
|
-
Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
|
|
1445
|
+
Content-Disposition: form-data; name="Filedata"; filename="${sanitizeFileName(fileName)}"\r
|
|
1297
1446
|
Content-Type: application/octet-stream\r
|
|
1298
1447
|
\r
|
|
1299
1448
|
`
|
|
@@ -1318,7 +1467,7 @@ Content-Type: application/octet-stream\r
|
|
|
1318
1467
|
return await uploadRes.json();
|
|
1319
1468
|
}
|
|
1320
1469
|
async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
1321
|
-
const url = `${BASE_URL3}/users/${userId}/drive/files/${fileId}/download`;
|
|
1470
|
+
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
1322
1471
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1323
1472
|
console.error(`[nworks] GET ${url} (get download URL)`);
|
|
1324
1473
|
}
|
|
@@ -1335,10 +1484,11 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
|
1335
1484
|
if (!redirectRes.ok) return handleError2(redirectRes);
|
|
1336
1485
|
throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
|
|
1337
1486
|
}
|
|
1487
|
+
const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
|
|
1338
1488
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1339
|
-
console.error(`[nworks] GET ${
|
|
1489
|
+
console.error(`[nworks] GET ${safeLocation} (download content)`);
|
|
1340
1490
|
}
|
|
1341
|
-
const downloadRes = await
|
|
1491
|
+
const downloadRes = await fetch(safeLocation, { method: "GET" });
|
|
1342
1492
|
if (!downloadRes.ok) return handleError2(downloadRes);
|
|
1343
1493
|
const arrayBuffer = await downloadRes.arrayBuffer();
|
|
1344
1494
|
const buffer = Buffer.from(arrayBuffer);
|
|
@@ -1475,7 +1625,7 @@ async function handleError3(res) {
|
|
|
1475
1625
|
async function sendMail(opts) {
|
|
1476
1626
|
const userId = opts.userId ?? "me";
|
|
1477
1627
|
const profile = opts.profile ?? "default";
|
|
1478
|
-
const url = `${BASE_URL4}/users/${userId}/mail`;
|
|
1628
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail`;
|
|
1479
1629
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1480
1630
|
console.error(`[nworks] POST ${url}`);
|
|
1481
1631
|
}
|
|
@@ -1504,7 +1654,7 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1504
1654
|
params.set("count", String(count));
|
|
1505
1655
|
if (cursor) params.set("cursor", cursor);
|
|
1506
1656
|
if (isUnread) params.set("isUnread", "true");
|
|
1507
|
-
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()}`;
|
|
1508
1658
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1509
1659
|
console.error(`[nworks] GET ${url}`);
|
|
1510
1660
|
}
|
|
@@ -1520,7 +1670,7 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1520
1670
|
};
|
|
1521
1671
|
}
|
|
1522
1672
|
async function readMail(mailId, userId = "me", profile = "default") {
|
|
1523
|
-
const url = `${BASE_URL4}/users/${userId}/mail/${mailId}`;
|
|
1673
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}`;
|
|
1524
1674
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1525
1675
|
console.error(`[nworks] GET ${url}`);
|
|
1526
1676
|
}
|
|
@@ -1644,7 +1794,7 @@ async function handleError4(res) {
|
|
|
1644
1794
|
}
|
|
1645
1795
|
async function resolveUserId(userId, profile) {
|
|
1646
1796
|
if (userId !== "me") return userId;
|
|
1647
|
-
const url = `${BASE_URL5}/users
|
|
1797
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
|
|
1648
1798
|
const res = await authedFetch4(url, { method: "GET" }, profile);
|
|
1649
1799
|
if (!res.ok) return handleError4(res);
|
|
1650
1800
|
const data = await res.json();
|
|
@@ -1659,7 +1809,7 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
|
|
|
1659
1809
|
params.set("count", String(count));
|
|
1660
1810
|
params.set("status", status);
|
|
1661
1811
|
if (cursor) params.set("cursor", cursor);
|
|
1662
|
-
const url = `${BASE_URL5}/users/${userId}/tasks?${params.toString()}`;
|
|
1812
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks?${params.toString()}`;
|
|
1663
1813
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1664
1814
|
console.error(`[nworks] GET ${url}`);
|
|
1665
1815
|
}
|
|
@@ -1683,10 +1833,10 @@ async function createTask(opts) {
|
|
|
1683
1833
|
};
|
|
1684
1834
|
if (opts.dueDate) body.dueDate = opts.dueDate;
|
|
1685
1835
|
if (opts.categoryId) body.categoryId = opts.categoryId;
|
|
1686
|
-
const url = `${BASE_URL5}/users/${userId}/tasks`;
|
|
1836
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks`;
|
|
1687
1837
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1688
1838
|
console.error(`[nworks] POST ${url}`);
|
|
1689
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1839
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1690
1840
|
}
|
|
1691
1841
|
const res = await authedFetch4(
|
|
1692
1842
|
url,
|
|
@@ -1709,7 +1859,7 @@ async function updateTask(opts) {
|
|
|
1709
1859
|
if (opts.title !== void 0) body.title = opts.title;
|
|
1710
1860
|
if (opts.content !== void 0) body.content = opts.content;
|
|
1711
1861
|
if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
|
|
1712
|
-
const url = `${BASE_URL5}/tasks/${opts.taskId}`;
|
|
1862
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(opts.taskId)}`;
|
|
1713
1863
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1714
1864
|
console.error(`[nworks] PATCH ${url}`);
|
|
1715
1865
|
}
|
|
@@ -1726,7 +1876,7 @@ async function updateTask(opts) {
|
|
|
1726
1876
|
return await res.json();
|
|
1727
1877
|
}
|
|
1728
1878
|
async function completeTask(taskId, profile = "default") {
|
|
1729
|
-
const url = `${BASE_URL5}/tasks/${taskId}/complete`;
|
|
1879
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/complete`;
|
|
1730
1880
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1731
1881
|
console.error(`[nworks] POST ${url}`);
|
|
1732
1882
|
}
|
|
@@ -1739,7 +1889,7 @@ async function completeTask(taskId, profile = "default") {
|
|
|
1739
1889
|
if (!res.ok) return handleError4(res);
|
|
1740
1890
|
}
|
|
1741
1891
|
async function incompleteTask(taskId, profile = "default") {
|
|
1742
|
-
const url = `${BASE_URL5}/tasks/${taskId}/incomplete`;
|
|
1892
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
|
|
1743
1893
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1744
1894
|
console.error(`[nworks] POST ${url}`);
|
|
1745
1895
|
}
|
|
@@ -1752,7 +1902,7 @@ async function incompleteTask(taskId, profile = "default") {
|
|
|
1752
1902
|
if (!res.ok) return handleError4(res);
|
|
1753
1903
|
}
|
|
1754
1904
|
async function deleteTask(taskId, profile = "default") {
|
|
1755
|
-
const url = `${BASE_URL5}/tasks/${taskId}`;
|
|
1905
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
|
|
1756
1906
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1757
1907
|
console.error(`[nworks] DELETE ${url}`);
|
|
1758
1908
|
}
|
|
@@ -1924,7 +2074,7 @@ async function listBoards(count = 20, cursor, profile = "default") {
|
|
|
1924
2074
|
if (!res.ok) return handleError5(res);
|
|
1925
2075
|
const text = await res.text();
|
|
1926
2076
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1927
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2077
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1928
2078
|
}
|
|
1929
2079
|
const data = safeParseJson(text);
|
|
1930
2080
|
return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
|
|
@@ -1933,7 +2083,7 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1933
2083
|
const params = new URLSearchParams();
|
|
1934
2084
|
params.set("count", String(count));
|
|
1935
2085
|
if (cursor) params.set("cursor", cursor);
|
|
1936
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts?${params.toString()}`;
|
|
2086
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts?${params.toString()}`;
|
|
1937
2087
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1938
2088
|
console.error(`[nworks] GET ${url}`);
|
|
1939
2089
|
}
|
|
@@ -1941,13 +2091,13 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1941
2091
|
if (!res.ok) return handleError5(res);
|
|
1942
2092
|
const text = await res.text();
|
|
1943
2093
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1944
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2094
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1945
2095
|
}
|
|
1946
2096
|
const data = safeParseJson(text);
|
|
1947
2097
|
return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
|
|
1948
2098
|
}
|
|
1949
2099
|
async function readPost(boardId, postId, profile = "default") {
|
|
1950
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts/${postId}`;
|
|
2100
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts/${sanitizePathSegment(postId)}`;
|
|
1951
2101
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1952
2102
|
console.error(`[nworks] GET ${url}`);
|
|
1953
2103
|
}
|
|
@@ -1955,7 +2105,7 @@ async function readPost(boardId, postId, profile = "default") {
|
|
|
1955
2105
|
if (!res.ok) return handleError5(res);
|
|
1956
2106
|
const text = await res.text();
|
|
1957
2107
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1958
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2108
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1959
2109
|
}
|
|
1960
2110
|
return safeParseJson(text);
|
|
1961
2111
|
}
|
|
@@ -1967,10 +2117,10 @@ async function createPost(opts) {
|
|
|
1967
2117
|
};
|
|
1968
2118
|
if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
|
|
1969
2119
|
if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
|
|
1970
|
-
const url = `${BASE_URL6}/boards/${opts.boardId}/posts`;
|
|
2120
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(opts.boardId)}/posts`;
|
|
1971
2121
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1972
2122
|
console.error(`[nworks] POST ${url}`);
|
|
1973
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
2123
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1974
2124
|
}
|
|
1975
2125
|
const res = await authedFetch5(
|
|
1976
2126
|
url,
|
|
@@ -1984,7 +2134,7 @@ async function createPost(opts) {
|
|
|
1984
2134
|
if (res.status === 201 || res.ok) {
|
|
1985
2135
|
const text = await res.text();
|
|
1986
2136
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1987
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2137
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1988
2138
|
}
|
|
1989
2139
|
return safeParseJson(text);
|
|
1990
2140
|
}
|
|
@@ -2280,7 +2430,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2280
2430
|
success: true,
|
|
2281
2431
|
message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
2282
2432
|
nextSteps,
|
|
2283
|
-
clientId,
|
|
2433
|
+
clientId: mask(clientId),
|
|
2284
2434
|
clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
|
|
2285
2435
|
serviceAccount: serviceAccount ?? null,
|
|
2286
2436
|
privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
|
|
@@ -2416,7 +2566,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2416
2566
|
sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
2417
2567
|
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2418
2568
|
},
|
|
2419
|
-
async ({
|
|
2569
|
+
async ({
|
|
2570
|
+
summary,
|
|
2571
|
+
start,
|
|
2572
|
+
end,
|
|
2573
|
+
timeZone,
|
|
2574
|
+
description,
|
|
2575
|
+
location,
|
|
2576
|
+
attendees,
|
|
2577
|
+
sendNotification,
|
|
2578
|
+
userId
|
|
2579
|
+
}) => {
|
|
2420
2580
|
try {
|
|
2421
2581
|
const result = await createEvent({
|
|
2422
2582
|
summary,
|
|
@@ -2455,7 +2615,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2455
2615
|
sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
2456
2616
|
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2457
2617
|
},
|
|
2458
|
-
async ({
|
|
2618
|
+
async ({
|
|
2619
|
+
eventId,
|
|
2620
|
+
summary,
|
|
2621
|
+
start,
|
|
2622
|
+
end,
|
|
2623
|
+
timeZone,
|
|
2624
|
+
description,
|
|
2625
|
+
location,
|
|
2626
|
+
sendNotification,
|
|
2627
|
+
userId
|
|
2628
|
+
}) => {
|
|
2459
2629
|
try {
|
|
2460
2630
|
await updateEvent({
|
|
2461
2631
|
eventId,
|
|
@@ -2568,11 +2738,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2568
2738
|
overwrite ?? false
|
|
2569
2739
|
);
|
|
2570
2740
|
} else if (filePath) {
|
|
2741
|
+
const safePath = validateLocalPath(filePath);
|
|
2571
2742
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2572
|
-
console.error(`[nworks] MCP upload: filePath=${
|
|
2743
|
+
console.error(`[nworks] MCP upload: filePath=${safePath}`);
|
|
2573
2744
|
}
|
|
2574
2745
|
result = await uploadFile(
|
|
2575
|
-
|
|
2746
|
+
safePath,
|
|
2576
2747
|
userId ?? "me",
|
|
2577
2748
|
folderId,
|
|
2578
2749
|
overwrite ?? false
|
|
@@ -2587,10 +2758,11 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2587
2758
|
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
2588
2759
|
};
|
|
2589
2760
|
} catch (err) {
|
|
2590
|
-
|
|
2591
|
-
|
|
2761
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2762
|
+
console.error(`[nworks] drive upload error: ${err.stack}`);
|
|
2763
|
+
}
|
|
2592
2764
|
return {
|
|
2593
|
-
content: [{ type: "text", text:
|
|
2765
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
|
|
2594
2766
|
isError: true
|
|
2595
2767
|
};
|
|
2596
2768
|
}
|
|
@@ -2615,7 +2787,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2615
2787
|
if (outputDir) {
|
|
2616
2788
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
2617
2789
|
const { join: join3 } = await import("path");
|
|
2618
|
-
const
|
|
2790
|
+
const safeDir = validateLocalPath(outputDir);
|
|
2791
|
+
const safeName = sanitizeFileName(fileName);
|
|
2792
|
+
const outPath = join3(safeDir, safeName);
|
|
2793
|
+
validateLocalPath(outPath, safeDir);
|
|
2619
2794
|
await writeFile3(outPath, result.buffer);
|
|
2620
2795
|
return {
|
|
2621
2796
|
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
@@ -3036,9 +3211,9 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3036
3211
|
const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
3037
3212
|
const requestedScopes = expandScopes((scope ?? DEFAULT_SCOPE).split(" ").filter(Boolean));
|
|
3038
3213
|
const mergedScopes = [.../* @__PURE__ */ new Set([...existingScopes, ...requestedScopes])].join(" ");
|
|
3039
|
-
const state =
|
|
3214
|
+
const state = generateSecureState();
|
|
3040
3215
|
const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
|
|
3041
|
-
startOAuthCallbackServer(creds.clientId, creds.clientSecret).then(
|
|
3216
|
+
startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
|
|
3042
3217
|
(token) => saveUserToken({
|
|
3043
3218
|
accessToken: token.accessToken,
|
|
3044
3219
|
refreshToken: token.refreshToken,
|
|
@@ -3083,9 +3258,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3083
3258
|
const userToken = await loadUserToken();
|
|
3084
3259
|
const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
|
|
3085
3260
|
const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
|
|
3261
|
+
const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
3086
3262
|
const info = {
|
|
3087
3263
|
serviceAccount: creds.serviceAccount ?? null,
|
|
3088
|
-
clientId: creds.clientId,
|
|
3264
|
+
clientId: mask(creds.clientId),
|
|
3089
3265
|
botId: creds.botId ?? null,
|
|
3090
3266
|
tokenValid: isValid,
|
|
3091
3267
|
userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
|
|
@@ -3107,6 +3283,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3107
3283
|
{},
|
|
3108
3284
|
async () => {
|
|
3109
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
|
+
}
|
|
3110
3298
|
await clearCredentials();
|
|
3111
3299
|
return {
|
|
3112
3300
|
content: [
|
|
@@ -3134,8 +3322,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3134
3322
|
async () => {
|
|
3135
3323
|
try {
|
|
3136
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
|
+
});
|
|
3137
3338
|
return {
|
|
3138
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
3339
|
+
content: [{ type: "text", text: JSON.stringify(maskedResults) }]
|
|
3139
3340
|
};
|
|
3140
3341
|
} catch (err) {
|
|
3141
3342
|
return {
|