@massu/core 1.15.2 → 1.15.3

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 (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli.js +89 -12
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.15.3] - 2026-07-06
8
+
9
+ **`massu login` hung forever in non-interactive shells (CI / scripts / agents) with `MASSU_API_KEY` set** (`plan-2026-07-06-login-noninteractive-env-hang`). `runLogin()` never read the `MASSU_API_KEY` environment variable and then read a non-TTY stdin unbounded (`for await … process.stdin`, blocking on an EOF that never arrives on an idle non-TTY stdin), so the documented, non-exposing scripted path (`MASSU_API_KEY=<key> massu login`) blocked indefinitely — the only working non-interactive path was `--key`, which leaks the key to shell history. Patch per semver: a bug fix with no API change, scoped entirely to the `login` command. Incident #1.
10
+
11
+ ### Fixed
12
+
13
+ - **`massu login` no longer hangs without a TTY.** Key resolution is now a pure, dependency-injected `resolveLoginKey()` with explicit precedence: `--key` > `MASSU_API_KEY` env > TTY prompt > bounded non-TTY stdin. The env var is read before any stdin touch (the reported hang case), and the non-TTY read is bounded by `MASSU_LOGIN_STDIN_TIMEOUT_MS` (default 2000ms) and capped at 64KB, so an idle or oversized stdin fails fast (fail-closed) with an actionable message instead of hanging; `echo "$KEY" | massu login` still works. A structural regression guard makes the interactive prompt / blocking stdin read unreachable when a key is available and stdin is not a TTY, and a help↔code contract test asserts `login` honors the advertised `MASSU_API_KEY`.
14
+
15
+ ### Added
16
+
17
+ - **`--key` now prints a shell-history exposure warning** to stderr when a key is passed via the flag, steering scripted/CI usage toward `MASSU_API_KEY` or a piped key.
18
+
7
19
  ## [1.15.2] - 2026-07-06
8
20
 
9
21
  **CRITICAL production fix: the `validate-key` cloud edge function returned HTTP 500 on every real API key (deploy drift), silently downgrading every Enterprise/Pro customer to Free** (`plan-2026-07-06-validate-key-deploy-drift`). The DEPLOYED `validate-key` Supabase edge function was a stale bundle that never received the 2026-05-31 `compareSync` fix — it still called the async bcrypt `compare()`, which spawns a Web Worker the Supabase Edge Runtime forbids, so it threw → HTTP 500 on every real `ms_live_` key. `@massu/core`'s license path (`validateLicense`) treated the 500 as "Free", so every paid key silently resolved to Free (surfaced during git-safe-api-key dogfooding, 2026-07-05). The repo source was already correct; the fix redeploys it (wrong-key probe now returns 401, not 500) — **no customer key changes, no re-hashing, no bcrypt/WebCrypto re-engineering** (the cause was a missed deploy, not a library bug). Patch per semver: a bug fix with no API change. Incident: `docs/incidents/2026-07-06-validate-key-deploy-drift-500.md`.
package/dist/cli.js CHANGED
@@ -15693,10 +15693,51 @@ var init_doctor = __esm({
15693
15693
  // src/commands/login.ts
15694
15694
  var login_exports = {};
15695
15695
  __export(login_exports, {
15696
+ MASSU_ENV_STDIN_TIMEOUT: () => MASSU_ENV_STDIN_TIMEOUT,
15697
+ MAX_STDIN_BYTES: () => MAX_STDIN_BYTES,
15698
+ NO_KEY_MESSAGE: () => NO_KEY_MESSAGE,
15699
+ NoLoginKeyError: () => NoLoginKeyError,
15700
+ loginStdinTimeoutMs: () => loginStdinTimeoutMs,
15701
+ resolveLoginKey: () => resolveLoginKey,
15696
15702
  runLogin: () => runLogin,
15697
15703
  runLogout: () => runLogout
15698
15704
  });
15699
15705
  import { createInterface as createInterface2 } from "readline";
15706
+ function loginStdinTimeoutMs(env) {
15707
+ const raw = env[MASSU_ENV_STDIN_TIMEOUT];
15708
+ const n = raw != null && raw !== "" ? Number(raw) : NaN;
15709
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_STDIN_TIMEOUT_MS;
15710
+ }
15711
+ async function readStdinBounded(readStdin, timeoutMs) {
15712
+ let timer;
15713
+ const read = readStdin();
15714
+ read.catch(() => void 0);
15715
+ const timeout = new Promise((_resolve, reject) => {
15716
+ timer = setTimeout(() => reject(new NoLoginKeyError()), timeoutMs);
15717
+ if (timer && typeof timer.unref === "function") timer.unref();
15718
+ });
15719
+ try {
15720
+ return await Promise.race([read, timeout]);
15721
+ } finally {
15722
+ if (timer) clearTimeout(timer);
15723
+ }
15724
+ }
15725
+ async function resolveLoginKey(deps) {
15726
+ const { argv, env, isTTY, readStdin, prompt, timeoutMs } = deps;
15727
+ const keyFlagIdx = argv.indexOf("--key");
15728
+ if (keyFlagIdx >= 0 && argv[keyFlagIdx + 1]) {
15729
+ return { key: argv[keyFlagIdx + 1], source: "flag" };
15730
+ }
15731
+ const envRaw = env[MASSU_ENV_API_KEY];
15732
+ const envKey = typeof envRaw === "string" ? envRaw.trim() : "";
15733
+ if (envKey.length > 0) {
15734
+ return { key: envKey, source: "env" };
15735
+ }
15736
+ if (isTTY) {
15737
+ return { key: await prompt("Massu API key: "), source: "prompt" };
15738
+ }
15739
+ return { key: await readStdinBounded(readStdin, timeoutMs), source: "stdin" };
15740
+ }
15700
15741
  function maskKey(k3) {
15701
15742
  return k3.length <= 12 ? "****" : `${k3.slice(0, API_KEY_PREFIX.length)}\u2026${k3.slice(-4)}`;
15702
15743
  }
@@ -15720,23 +15761,46 @@ function promptHidden(question) {
15720
15761
  }
15721
15762
  async function readStdinKey() {
15722
15763
  const chunks = [];
15764
+ let total = 0;
15723
15765
  for await (const chunk of process.stdin) {
15724
- chunks.push(chunk);
15766
+ const buf = chunk;
15767
+ total += buf.length;
15768
+ if (total > MAX_STDIN_BYTES) {
15769
+ process.stdin.destroy?.();
15770
+ throw new NoLoginKeyError(
15771
+ `Piped input exceeds ${MAX_STDIN_BYTES} bytes \u2014 an API key is under 100 bytes. Aborting; nothing was written.`
15772
+ );
15773
+ }
15774
+ chunks.push(buf);
15725
15775
  }
15726
15776
  return Buffer.concat(chunks).toString("utf-8");
15727
15777
  }
15728
15778
  async function runLogin(argv) {
15729
- const keyFlagIdx = argv.indexOf("--key");
15730
15779
  const noVerify = argv.includes("--no-verify");
15731
- let key;
15732
- if (keyFlagIdx >= 0 && argv[keyFlagIdx + 1]) {
15733
- key = argv[keyFlagIdx + 1];
15734
- } else if (process.stdin.isTTY) {
15735
- key = await promptHidden("Massu API key: ");
15736
- } else {
15737
- key = await readStdinKey();
15780
+ let resolved;
15781
+ try {
15782
+ resolved = await resolveLoginKey({
15783
+ argv,
15784
+ env: process.env,
15785
+ isTTY: Boolean(process.stdin.isTTY),
15786
+ readStdin: readStdinKey,
15787
+ prompt: promptHidden,
15788
+ timeoutMs: loginStdinTimeoutMs(process.env)
15789
+ });
15790
+ } catch (err) {
15791
+ if (err instanceof NoLoginKeyError) {
15792
+ console.error(`Error: ${err.message}`);
15793
+ process.exit(1);
15794
+ return;
15795
+ }
15796
+ throw err;
15797
+ }
15798
+ if (resolved.source === "flag") {
15799
+ console.error(
15800
+ 'Warning: --key exposes your API key to shell history. Prefer MASSU_API_KEY or a piped key (echo "$KEY" | massu login) for automation.'
15801
+ );
15738
15802
  }
15739
- key = (key ?? "").trim();
15803
+ const key = (resolved.key ?? "").trim();
15740
15804
  if (!key.startsWith(API_KEY_PREFIX)) {
15741
15805
  console.error(`Error: API key must start with "${API_KEY_PREFIX}".`);
15742
15806
  process.exit(1);
@@ -15791,10 +15855,21 @@ async function runLogout() {
15791
15855
  removed ? `Removed stored credentials at ${credentialsPath()}.` : "No stored credentials to remove."
15792
15856
  );
15793
15857
  }
15858
+ var DEFAULT_STDIN_TIMEOUT_MS, MASSU_ENV_STDIN_TIMEOUT, NO_KEY_MESSAGE, NoLoginKeyError, MAX_STDIN_BYTES;
15794
15859
  var init_login = __esm({
15795
15860
  "src/commands/login.ts"() {
15796
15861
  "use strict";
15797
15862
  init_credentials();
15863
+ DEFAULT_STDIN_TIMEOUT_MS = 2e3;
15864
+ MASSU_ENV_STDIN_TIMEOUT = "MASSU_LOGIN_STDIN_TIMEOUT_MS";
15865
+ NO_KEY_MESSAGE = `No API key found. Set ${MASSU_ENV_API_KEY}, pipe the key (echo "$KEY" | massu login), or pass --key <k>; the interactive prompt needs a TTY.`;
15866
+ NoLoginKeyError = class extends Error {
15867
+ constructor(message = NO_KEY_MESSAGE) {
15868
+ super(message);
15869
+ this.name = "NoLoginKeyError";
15870
+ }
15871
+ };
15872
+ MAX_STDIN_BYTES = 64 * 1024;
15798
15873
  }
15799
15874
  });
15800
15875
 
@@ -31044,8 +31119,10 @@ Commands:
31044
31119
  init Set up Massu AI in your project (one command, full setup)
31045
31120
  doctor Check installation health
31046
31121
  login Store your API key user-level in ~/.massu/credentials (git-safe).
31047
- Prefer the interactive prompt or MASSU_API_KEY env; --key <k>
31048
- exposes the key to shell history. Use --no-verify to skip online check.
31122
+ Interactive: hidden prompt. Non-interactive (CI/agent): set
31123
+ MASSU_API_KEY (recommended), or pipe (echo "$KEY" | massu login);
31124
+ --key <k> works but exposes the key to shell history. Never hangs
31125
+ without a TTY. Use --no-verify to skip the online check.
31049
31126
  logout Remove the stored user-level API key
31050
31127
  install-hooks Install/update Claude Code hooks
31051
31128
  install-commands Install/update slash commands (use --skip-permissions to opt out of MCP allowlist seeding)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.15.2",
3
+ "version": "1.15.3",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 56 workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "./dist/cli.js",