@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.
Files changed (2) hide show
  1. package/dist/index.js +116 -77
  2. 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.set("Content-Type", "application/json");
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
- * Returns access_token on success, or indicates TOTP is required.
288
- * On 401: throws MSaplingError with code 'invalid_credentials'
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 response = await fetch(`${this.apiUrl}/auth/login-verify`, {
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
- if (result.kind === "totp_required") {
8015
- context.addMessage("system", "Two-factor authentication required.");
8016
- let totpCode = "";
8017
- try {
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.7"
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.7",
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",