@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.
- package/CHANGELOG.md +12 -0
- package/dist/cli.js +89 -12
- 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
|
-
|
|
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
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
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
|
-
|
|
31048
|
-
|
|
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.
|
|
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",
|