@mtreeai/msapling-cli 2.3.6-beta.7 → 2.3.6-beta.9
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/dist/index.js +116 -77
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -79,6 +79,11 @@ var init_src = __esm({
|
|
|
79
79
|
MSaplingClient = class {
|
|
80
80
|
apiUrl;
|
|
81
81
|
token;
|
|
82
|
+
// CLI-AUTH-CSRF-01: simple cookie jar so we can implement the backend's
|
|
83
|
+
// double-submit CSRF pattern (msapling_csrftoken cookie ↔ X-CSRF-Token header).
|
|
84
|
+
// Without this, every authenticated POST/PATCH/DELETE returns 403
|
|
85
|
+
// CSRF_COOKIE_MISSING even with a valid Bearer token.
|
|
86
|
+
cookies = /* @__PURE__ */ new Map();
|
|
82
87
|
constructor(options = {}) {
|
|
83
88
|
this.apiUrl = (options.apiUrl || "https://api.msapling.com").replace(/\/$/, "");
|
|
84
89
|
this.token = options.token || null;
|
|
@@ -89,12 +94,38 @@ var init_src = __esm({
|
|
|
89
94
|
getApiUrl() {
|
|
90
95
|
return this.apiUrl;
|
|
91
96
|
}
|
|
97
|
+
serializeCookies() {
|
|
98
|
+
return Array.from(this.cookies.entries()).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
99
|
+
}
|
|
100
|
+
updateCookiesFromResponse(response) {
|
|
101
|
+
const setCookieArr = response.headers.getSetCookie?.() ?? [];
|
|
102
|
+
for (const line of setCookieArr) {
|
|
103
|
+
const firstSemi = line.indexOf(";");
|
|
104
|
+
const pair = firstSemi >= 0 ? line.slice(0, firstSemi) : line;
|
|
105
|
+
const eq = pair.indexOf("=");
|
|
106
|
+
if (eq <= 0) continue;
|
|
107
|
+
const name = pair.slice(0, eq).trim();
|
|
108
|
+
const value = pair.slice(eq + 1).trim();
|
|
109
|
+
if (!name || !value) continue;
|
|
110
|
+
this.cookies.set(name, value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
92
113
|
async request(path2, options = {}) {
|
|
93
114
|
const headers = new Headers(options.headers);
|
|
94
115
|
if (this.token) {
|
|
95
116
|
headers.set("Authorization", `Bearer ${this.token}`);
|
|
96
117
|
}
|
|
97
|
-
headers.
|
|
118
|
+
if (!headers.has("Content-Type")) {
|
|
119
|
+
headers.set("Content-Type", "application/json");
|
|
120
|
+
}
|
|
121
|
+
if (this.cookies.size > 0) {
|
|
122
|
+
headers.set("Cookie", this.serializeCookies());
|
|
123
|
+
}
|
|
124
|
+
const method = (options.method || "GET").toUpperCase();
|
|
125
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
126
|
+
const csrf = this.cookies.get("msapling_csrftoken");
|
|
127
|
+
if (csrf) headers.set("X-CSRF-Token", csrf);
|
|
128
|
+
}
|
|
98
129
|
const controller = new AbortController();
|
|
99
130
|
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
100
131
|
try {
|
|
@@ -106,6 +137,7 @@ var init_src = __esm({
|
|
|
106
137
|
keepalive: true
|
|
107
138
|
});
|
|
108
139
|
clearTimeout(timeout);
|
|
140
|
+
this.updateCookiesFromResponse(response);
|
|
109
141
|
if (!response.ok) {
|
|
110
142
|
let detail = "Unknown error";
|
|
111
143
|
let code = "unknown";
|
|
@@ -284,8 +316,19 @@ var init_src = __esm({
|
|
|
284
316
|
}
|
|
285
317
|
/**
|
|
286
318
|
* Login with email and password.
|
|
287
|
-
*
|
|
288
|
-
*
|
|
319
|
+
*
|
|
320
|
+
* The JWT `mfa` claim is the *session* MFA state ("has this session completed
|
|
321
|
+
* TOTP yet"), not "TOTP is required". Treating `mfa:false` as TOTP-required
|
|
322
|
+
* was wrong: it blocked every user without 2FA enabled, including all
|
|
323
|
+
* guest/free accounts and the live backend which doesn't enforce TOTP yet.
|
|
324
|
+
*
|
|
325
|
+
* New contract: accept the token as fully usable. If the backend enforces
|
|
326
|
+
* MFA on a subsequent protected endpoint, that endpoint will reply with
|
|
327
|
+
* `mfa_required` and the caller can step up via verifyLoginTotp(). Until
|
|
328
|
+
* the backend exposes an explicit `mfa_required` field on the login
|
|
329
|
+
* response, we don't pre-emptively branch.
|
|
330
|
+
*
|
|
331
|
+
* On 401: throws MSaplingError with code 'invalid_credentials'.
|
|
289
332
|
*/
|
|
290
333
|
async loginEmailPassword(email, password) {
|
|
291
334
|
const params = new URLSearchParams();
|
|
@@ -303,12 +346,9 @@ var init_src = __esm({
|
|
|
303
346
|
signal: controller.signal
|
|
304
347
|
});
|
|
305
348
|
clearTimeout(timeout);
|
|
349
|
+
this.updateCookiesFromResponse(response);
|
|
306
350
|
if (response.ok) {
|
|
307
351
|
const data = await response.json();
|
|
308
|
-
const payload = this.decodeJwtPayload(data.access_token);
|
|
309
|
-
if (payload && payload.mfa === false) {
|
|
310
|
-
return { kind: "totp_required", partialToken: data.access_token };
|
|
311
|
-
}
|
|
312
352
|
return { kind: "success", token: data.access_token };
|
|
313
353
|
}
|
|
314
354
|
if (response.status === 401) {
|
|
@@ -330,6 +370,42 @@ var init_src = __esm({
|
|
|
330
370
|
clearTimeout(timeout);
|
|
331
371
|
}
|
|
332
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Sign in as a guest. Backend issues a guest-scoped JWT with limited tier
|
|
375
|
+
* capabilities. No prior credentials required.
|
|
376
|
+
*/
|
|
377
|
+
async loginGuest() {
|
|
378
|
+
const controller = new AbortController();
|
|
379
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
380
|
+
try {
|
|
381
|
+
const response = await fetch(`${this.apiUrl}/auth/guest`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "Content-Type": "application/json" },
|
|
384
|
+
body: "{}",
|
|
385
|
+
signal: controller.signal
|
|
386
|
+
});
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
this.updateCookiesFromResponse(response);
|
|
389
|
+
if (response.ok) {
|
|
390
|
+
const data = await response.json();
|
|
391
|
+
return { kind: "success", token: data.access_token };
|
|
392
|
+
}
|
|
393
|
+
let detail = "Guest login failed";
|
|
394
|
+
try {
|
|
395
|
+
const err = await response.json();
|
|
396
|
+
detail = err.detail || detail;
|
|
397
|
+
} catch (e) {
|
|
398
|
+
}
|
|
399
|
+
throw new MSaplingError(detail, response.status, "guest_login_failed");
|
|
400
|
+
} catch (e) {
|
|
401
|
+
if (e.name === "AbortError") {
|
|
402
|
+
throw new MSaplingError("Request timed out after 30s.", 408, "timeout");
|
|
403
|
+
}
|
|
404
|
+
throw e;
|
|
405
|
+
} finally {
|
|
406
|
+
clearTimeout(timeout);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
333
409
|
/**
|
|
334
410
|
* Verify TOTP code during login.
|
|
335
411
|
* Requires a partial token from a prior /auth/login call when MFA is enabled.
|
|
@@ -339,16 +415,21 @@ var init_src = __esm({
|
|
|
339
415
|
const controller = new AbortController();
|
|
340
416
|
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
341
417
|
try {
|
|
342
|
-
const
|
|
418
|
+
const csrf = this.cookies.get("msapling_csrftoken");
|
|
419
|
+
const verifyHeaders = {
|
|
420
|
+
"Authorization": `Bearer ${this.token}`,
|
|
421
|
+
"Content-Type": "application/json"
|
|
422
|
+
};
|
|
423
|
+
if (this.cookies.size > 0) verifyHeaders["Cookie"] = this.serializeCookies();
|
|
424
|
+
if (csrf) verifyHeaders["X-CSRF-Token"] = csrf;
|
|
425
|
+
const response = await fetch(`${this.apiUrl}/api/mfa/login-verify`, {
|
|
343
426
|
method: "POST",
|
|
344
|
-
headers:
|
|
345
|
-
"Authorization": `Bearer ${this.token}`,
|
|
346
|
-
"Content-Type": "application/json"
|
|
347
|
-
},
|
|
427
|
+
headers: verifyHeaders,
|
|
348
428
|
body: JSON.stringify({ code }),
|
|
349
429
|
signal: controller.signal
|
|
350
430
|
});
|
|
351
431
|
clearTimeout(timeout);
|
|
432
|
+
this.updateCookiesFromResponse(response);
|
|
352
433
|
if (response.ok) {
|
|
353
434
|
const data = await response.json();
|
|
354
435
|
return { kind: "success", token: data.access_token };
|
|
@@ -7964,37 +8045,6 @@ async function promptPassword(prompt) {
|
|
|
7964
8045
|
stdin.on("data", onData);
|
|
7965
8046
|
});
|
|
7966
8047
|
}
|
|
7967
|
-
async function promptTotp(prompt) {
|
|
7968
|
-
return new Promise((resolve18) => {
|
|
7969
|
-
const stdin = process.stdin;
|
|
7970
|
-
const stdout = process.stdout;
|
|
7971
|
-
stdout.write(prompt);
|
|
7972
|
-
const wasRaw = stdin.isRaw ?? false;
|
|
7973
|
-
setRawModeGuarded(stdin, true);
|
|
7974
|
-
let code = "";
|
|
7975
|
-
const onData = (chunk) => {
|
|
7976
|
-
const char = chunk.toString();
|
|
7977
|
-
if (char === "\n" || char === "\r") {
|
|
7978
|
-
stdin.setRawMode(wasRaw);
|
|
7979
|
-
stdin.removeListener("data", onData);
|
|
7980
|
-
stdout.write("\n");
|
|
7981
|
-
resolve18(code);
|
|
7982
|
-
} else if (char === "") {
|
|
7983
|
-
setRawModeGuarded(stdin, wasRaw);
|
|
7984
|
-
stdin.removeListener("data", onData);
|
|
7985
|
-
stdout.write("\n");
|
|
7986
|
-
resolve18("");
|
|
7987
|
-
} else if (char === "\x7F" || char === "\b") {
|
|
7988
|
-
code = code.slice(0, -1);
|
|
7989
|
-
} else if (/^\d$/.test(char)) {
|
|
7990
|
-
if (code.length < 6) {
|
|
7991
|
-
code += char;
|
|
7992
|
-
}
|
|
7993
|
-
}
|
|
7994
|
-
};
|
|
7995
|
-
stdin.on("data", onData);
|
|
7996
|
-
});
|
|
7997
|
-
}
|
|
7998
8048
|
async function loginWithEmailPassword(email, context) {
|
|
7999
8049
|
context.addMessage("system", `Logging in as ${email}...`);
|
|
8000
8050
|
let password = "";
|
|
@@ -8011,40 +8061,26 @@ async function loginWithEmailPassword(email, context) {
|
|
|
8011
8061
|
try {
|
|
8012
8062
|
const result = await context.client.loginEmailPassword(email, password);
|
|
8013
8063
|
password = "";
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
totpCode = await promptTotp("Enter your 6-digit code: ");
|
|
8019
|
-
} catch (err) {
|
|
8020
|
-
context.addMessage("error", `TOTP prompt failed: ${err}`);
|
|
8021
|
-
return;
|
|
8022
|
-
}
|
|
8023
|
-
if (!totpCode || totpCode.length !== 6) {
|
|
8024
|
-
context.addMessage("error", "Invalid TOTP code (must be 6 digits).");
|
|
8025
|
-
return;
|
|
8026
|
-
}
|
|
8027
|
-
context.client.setToken(result.partialToken);
|
|
8028
|
-
try {
|
|
8029
|
-
const totpResult = await context.client.verifyLoginTotp(totpCode);
|
|
8030
|
-
await context.storage.saveToken(totpResult.token);
|
|
8031
|
-
context.client.setToken(totpResult.token);
|
|
8032
|
-
await context.refreshOverview();
|
|
8033
|
-
context.addMessage("system", `Logged in as ${email} (2FA verified).`);
|
|
8034
|
-
} catch (err) {
|
|
8035
|
-
context.client.setToken(null);
|
|
8036
|
-
context.addMessage("error", `TOTP verification failed: ${err}`);
|
|
8037
|
-
}
|
|
8038
|
-
} else if (result.kind === "success") {
|
|
8039
|
-
await context.storage.saveToken(result.token);
|
|
8040
|
-
context.client.setToken(result.token);
|
|
8041
|
-
await context.refreshOverview();
|
|
8042
|
-
context.addMessage("system", `Logged in as ${email}.`);
|
|
8043
|
-
}
|
|
8064
|
+
await context.storage.saveToken(result.token);
|
|
8065
|
+
context.client.setToken(result.token);
|
|
8066
|
+
await context.refreshOverview();
|
|
8067
|
+
context.addMessage("system", `Logged in as ${email}.`);
|
|
8044
8068
|
} catch (err) {
|
|
8045
8069
|
context.addMessage("error", `Login failed: ${err}`);
|
|
8046
8070
|
}
|
|
8047
8071
|
}
|
|
8072
|
+
async function loginAsGuest(context) {
|
|
8073
|
+
context.addMessage("system", "Signing in as guest...");
|
|
8074
|
+
try {
|
|
8075
|
+
const result = await context.client.loginGuest();
|
|
8076
|
+
await context.storage.saveToken(result.token);
|
|
8077
|
+
context.client.setToken(result.token);
|
|
8078
|
+
await context.refreshOverview();
|
|
8079
|
+
context.addMessage("system", "Signed in as guest (limited tier).");
|
|
8080
|
+
} catch (err) {
|
|
8081
|
+
context.addMessage("error", `Guest login failed: ${err}`);
|
|
8082
|
+
}
|
|
8083
|
+
}
|
|
8048
8084
|
async function loginWithGithubDevice(context) {
|
|
8049
8085
|
context.addMessage("system", "Starting GitHub device flow...");
|
|
8050
8086
|
let deviceResp;
|
|
@@ -8139,8 +8175,8 @@ var init_login = __esm({
|
|
|
8139
8175
|
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
8140
8176
|
loginCommand = {
|
|
8141
8177
|
name: "login",
|
|
8142
|
-
args: "[email|github|token]",
|
|
8143
|
-
description: "Authenticate: /login your@email.com | /login github | /login <token>",
|
|
8178
|
+
args: "[email|github|guest|token]",
|
|
8179
|
+
description: "Authenticate: /login your@email.com | /login github | /login guest | /login <token>",
|
|
8144
8180
|
category: "auth",
|
|
8145
8181
|
handler: async (args2, context) => {
|
|
8146
8182
|
const arg = args2[0];
|
|
@@ -8150,12 +8186,15 @@ var init_login = __esm({
|
|
|
8150
8186
|
`Usage:
|
|
8151
8187
|
/login your@email.com Sign in with email + password
|
|
8152
8188
|
/login github GitHub device flow
|
|
8189
|
+
/login guest Anonymous guest session (limited tier)
|
|
8153
8190
|
/login <token> Paste an API token from msapling.com \u2192 Settings \u2192 API Keys`
|
|
8154
8191
|
);
|
|
8155
8192
|
return;
|
|
8156
8193
|
}
|
|
8157
8194
|
if (arg === "github") {
|
|
8158
8195
|
await loginWithGithubDevice(context);
|
|
8196
|
+
} else if (arg === "guest") {
|
|
8197
|
+
await loginAsGuest(context);
|
|
8159
8198
|
} else if (isEmailAddress(arg)) {
|
|
8160
8199
|
await loginWithEmailPassword(arg, context);
|
|
8161
8200
|
} else if (isTokenLike(arg)) {
|
|
@@ -8166,7 +8205,7 @@ var init_login = __esm({
|
|
|
8166
8205
|
} else {
|
|
8167
8206
|
context.addMessage(
|
|
8168
8207
|
"error",
|
|
8169
|
-
`Unrecognized argument: "${arg}". Expected email, 'github', or token string.`
|
|
8208
|
+
`Unrecognized argument: "${arg}". Expected email, 'github', 'guest', or token string.`
|
|
8170
8209
|
);
|
|
8171
8210
|
}
|
|
8172
8211
|
}
|
|
@@ -13134,7 +13173,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
13134
13173
|
var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
|
|
13135
13174
|
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
13136
13175
|
"\u25CF MSapling CLI v",
|
|
13137
|
-
"2.3.6-beta.
|
|
13176
|
+
"2.3.6-beta.9"
|
|
13138
13177
|
] }),
|
|
13139
13178
|
/* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
|
|
13140
13179
|
] });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mtreeai/msapling-cli",
|
|
3
|
-
"version": "2.3.6-beta.
|
|
3
|
+
"version": "2.3.6-beta.9",
|
|
4
4
|
"description": "MSapling CLI — React/Ink terminal client for the MSapling backend (chat, projects, MDrive, agent tools). Proprietary; redistribution prohibited.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "MSapling Team",
|