@massu/core 1.10.4 → 1.10.6

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/cli.js CHANGED
@@ -14202,8 +14202,102 @@ var init_init = __esm({
14202
14202
  }
14203
14203
  });
14204
14204
 
14205
+ // src/security/license-pubkey.generated.ts
14206
+ var LICENSE_PUBKEY_ED25519, LICENSE_PUBKEY_FINGERPRINT_HEX, KNOWN_LICENSE_PUBKEY_FINGERPRINTS;
14207
+ var init_license_pubkey_generated = __esm({
14208
+ "src/security/license-pubkey.generated.ts"() {
14209
+ "use strict";
14210
+ LICENSE_PUBKEY_ED25519 = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
14211
+ LICENSE_PUBKEY_FINGERPRINT_HEX = "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475";
14212
+ KNOWN_LICENSE_PUBKEY_FINGERPRINTS = /* @__PURE__ */ new Set([
14213
+ "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
14214
+ ]);
14215
+ }
14216
+ });
14217
+
14218
+ // src/security/license-response-verifier.ts
14219
+ import { createPublicKey } from "crypto";
14220
+ function verifyLicenseResponse(payload) {
14221
+ if (!KNOWN_LICENSE_PUBKEY_FINGERPRINTS.has(LICENSE_PUBKEY_FINGERPRINT_HEX)) {
14222
+ return {
14223
+ kind: "error",
14224
+ reason: `Bundled license pubkey fingerprint ${LICENSE_PUBKEY_FINGERPRINT_HEX} is not in the trusted allowlist. Possible build-time tamper.`
14225
+ };
14226
+ }
14227
+ const sig = payload._signature;
14228
+ const alg = payload._signature_alg;
14229
+ const payloadKeys = payload._signature_payload_keys;
14230
+ const sigPubkey = payload._signature_pubkey_fingerprint;
14231
+ if (typeof sig !== "string" || sig.length === 0) {
14232
+ return { kind: "missing_signature" };
14233
+ }
14234
+ if (alg !== "ed25519") {
14235
+ return { kind: "error", reason: `Unsupported signature algorithm: ${alg}` };
14236
+ }
14237
+ if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
14238
+ return { kind: "error", reason: "Missing _signature_payload_keys" };
14239
+ }
14240
+ if (typeof sigPubkey === "string" && sigPubkey !== LICENSE_PUBKEY_FINGERPRINT_HEX) {
14241
+ return { kind: "unknown_pubkey", got: sigPubkey };
14242
+ }
14243
+ const canonicalObj = {};
14244
+ for (const k3 of payloadKeys) {
14245
+ if (typeof k3 !== "string") continue;
14246
+ canonicalObj[k3] = payload[k3];
14247
+ }
14248
+ const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
14249
+ try {
14250
+ const spkiPrefix = Buffer.from([
14251
+ 48,
14252
+ 42,
14253
+ 48,
14254
+ 5,
14255
+ 6,
14256
+ 3,
14257
+ 43,
14258
+ 101,
14259
+ 112,
14260
+ 3,
14261
+ 33,
14262
+ 0
14263
+ ]);
14264
+ const der = Buffer.concat([spkiPrefix, Buffer.from(LICENSE_PUBKEY_ED25519)]);
14265
+ const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
14266
+ const { verify } = __require("crypto");
14267
+ const ok = verify(
14268
+ null,
14269
+ Buffer.from(canonical, "utf-8"),
14270
+ pubkey,
14271
+ Buffer.from(sig, "base64")
14272
+ );
14273
+ return ok ? { kind: "valid" } : { kind: "bad_signature" };
14274
+ } catch (err) {
14275
+ return {
14276
+ kind: "error",
14277
+ reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`
14278
+ };
14279
+ }
14280
+ }
14281
+ function isLicenseSignatureRequired() {
14282
+ return process.env.MASSU_REQUIRE_SIGNED_LICENSE === "true";
14283
+ }
14284
+ var init_license_response_verifier = __esm({
14285
+ "src/security/license-response-verifier.ts"() {
14286
+ "use strict";
14287
+ init_license_pubkey_generated();
14288
+ }
14289
+ });
14290
+
14205
14291
  // src/license.ts
14206
14292
  import { createHash as createHash5 } from "crypto";
14293
+ function warnLicenseSigOnce(reason) {
14294
+ if (_warnedLicenseSig) return;
14295
+ _warnedLicenseSig = true;
14296
+ process.stderr.write(
14297
+ `[massu] WARNING: license-validate response is unsigned or signature invalid (${reason}). Acceptance permitted under transition mode. Operator: provision Supabase Edge Function LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64 then set MASSU_REQUIRE_SIGNED_LICENSE=true to enforce strict mode.
14298
+ `
14299
+ );
14300
+ }
14207
14301
  function tierLevel(tier) {
14208
14302
  return TIER_LEVELS[tier] ?? 0;
14209
14303
  }
@@ -14263,6 +14357,13 @@ async function validateLicense(apiKey) {
14263
14357
  });
14264
14358
  if (response.ok) {
14265
14359
  const data = await response.json();
14360
+ const sigResult = verifyLicenseResponse(data);
14361
+ if (sigResult.kind !== "valid") {
14362
+ if (isLicenseSignatureRequired()) {
14363
+ throw new Error(`License response signature invalid: ${sigResult.kind}`);
14364
+ }
14365
+ warnLicenseSigOnce(sigResult.kind);
14366
+ }
14266
14367
  if (data.valid) {
14267
14368
  const tier = data.plan ? PLAN_TO_TIER_MAP[data.plan] ?? "free" : data.tier ?? "free";
14268
14369
  const validUntil = data.validUntil ?? "";
@@ -14385,12 +14486,14 @@ async function handleLicenseToolCall(name, _args, _memDb) {
14385
14486
  }
14386
14487
  return { content: [{ type: "text", text: `Unknown license tool: ${name}` }] };
14387
14488
  }
14388
- var TIER_LEVELS, TOOL_TIER_MAP, PLAN_TO_TIER_MAP, TIER_LABELS, cachedTier, cachedTierTimestamp, IN_MEMORY_CACHE_TTL_MS;
14489
+ var _warnedLicenseSig, TIER_LEVELS, TOOL_TIER_MAP, PLAN_TO_TIER_MAP, TIER_LABELS, cachedTier, cachedTierTimestamp, IN_MEMORY_CACHE_TTL_MS;
14389
14490
  var init_license = __esm({
14390
14491
  "src/license.ts"() {
14391
14492
  "use strict";
14392
14493
  init_config();
14393
14494
  init_memory_db();
14495
+ init_license_response_verifier();
14496
+ _warnedLicenseSig = false;
14394
14497
  TIER_LEVELS = {
14395
14498
  free: 0,
14396
14499
  pro: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
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), 59 workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
@@ -24,7 +24,7 @@
24
24
  "build:bundle-adapters": "tsx scripts/bundle-adapters.ts",
25
25
  "build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
26
26
  "build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
27
- "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && npm run build",
27
+ "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && npm run build",
28
28
  "bench:watch": "tsx test/perf/watch-benchmark.ts"
29
29
  },
30
30
  "dependencies": {
package/src/license.ts CHANGED
@@ -17,6 +17,21 @@ import { createHash } from 'crypto';
17
17
  import type { ToolDefinition, ToolResult } from './tools.ts';
18
18
  import { getConfig } from './config.ts';
19
19
  import { getMemoryDb } from './memory-db.ts';
20
+ import { verifyLicenseResponse, isLicenseSignatureRequired } from './security/license-response-verifier.ts';
21
+
22
+ // P-H019 one-shot warning gate. We emit the stderr warning at most once
23
+ // per process lifetime so the customer's terminal isn't spammed every
24
+ // session-start until the operator provisions the Edge Function signing key.
25
+ let _warnedLicenseSig = false;
26
+ function warnLicenseSigOnce(reason: string): void {
27
+ if (_warnedLicenseSig) return;
28
+ _warnedLicenseSig = true;
29
+ process.stderr.write(
30
+ `[massu] WARNING: license-validate response is unsigned or signature invalid (${reason}). ` +
31
+ `Acceptance permitted under transition mode. Operator: provision Supabase Edge Function ` +
32
+ `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64 then set MASSU_REQUIRE_SIGNED_LICENSE=true to enforce strict mode.\n`,
33
+ );
34
+ }
20
35
 
21
36
  // ============================================================
22
37
  // Types
@@ -294,8 +309,35 @@ export async function validateLicense(apiKey: string): Promise<LicenseInfo> {
294
309
  validUntil?: string;
295
310
  features?: string[];
296
311
  reason?: string;
312
+ _signature?: string;
313
+ _signature_alg?: string;
314
+ _signature_payload_keys?: readonly string[];
315
+ _signature_pubkey_fingerprint?: string;
297
316
  };
298
317
 
318
+ // P-H019 (plan-stage-c-high-batch / 1.10.5): verify Ed25519
319
+ // signature on the validate-key response. Closes the bug class
320
+ // where MITM / malicious-cloud-endpoint / local SQLite edit
321
+ // could grant arbitrary tier.
322
+ //
323
+ // Transition mode (default): unsigned/invalid-sig responses are
324
+ // accepted with a one-shot stderr warning. Lets existing customers
325
+ // keep working while operators provision the Edge Function signing
326
+ // key.
327
+ //
328
+ // Strict mode (MASSU_REQUIRE_SIGNED_LICENSE=true, post-cutover):
329
+ // unsigned/invalid-sig responses are rejected; caller falls
330
+ // through to the grace-period cache or free tier.
331
+ const sigResult = verifyLicenseResponse(data);
332
+ if (sigResult.kind !== 'valid') {
333
+ if (isLicenseSignatureRequired()) {
334
+ // Strict mode — reject. Caller falls through to grace period.
335
+ throw new Error(`License response signature invalid: ${sigResult.kind}`);
336
+ }
337
+ // Transition mode — one-shot stderr warning per session.
338
+ warnLicenseSigOnce(sigResult.kind);
339
+ }
340
+
299
341
  if (data.valid) {
300
342
  // Map plan name to tier using PLAN_TO_TIER_MAP
301
343
  const tier: ToolTier = data.plan
@@ -0,0 +1,30 @@
1
+ // AUTO-GENERATED by scripts/bundle-license-pubkey.mjs at 2026-05-17T15:41:04.385Z.
2
+ // Source pem: packages/core/security/license-pubkey.pem
3
+ // RAW-bytes sha256: 18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475
4
+ // DO NOT EDIT — regenerate via `node scripts/bundle-license-pubkey.mjs` or
5
+ // the prepublishOnly hook (which runs on every npm publish).
6
+
7
+ /**
8
+ * Ed25519 public key for verifying signed /validate-key responses from
9
+ * massu.ai. Used by packages/core/src/security/license-response-verifier.ts
10
+ * to reject MITM / malicious-cloud-endpoint responses.
11
+ *
12
+ * P-H019 (plan-stage-c-high-batch / 1.10.5).
13
+ *
14
+ * To rotate: regenerate the keypair via Node crypto.generateKeyPairSync,
15
+ * update packages/core/security/license-pubkey.pem with the new public key,
16
+ * append the new RAW-bytes sha256 to KNOWN_PUBKEY_FINGERPRINTS in
17
+ * scripts/bundle-license-pubkey.mjs (DO NOT delete the old entry during
18
+ * the rotation grace window), then run `node scripts/bundle-license-pubkey.mjs`.
19
+ * Operator then updates the Supabase Edge Function env var
20
+ * `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` with the new private key.
21
+ */
22
+ export const LICENSE_PUBKEY_ED25519: Uint8Array = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
23
+
24
+ /** Hex sha256 of LICENSE_PUBKEY_ED25519 (raw 32 bytes). */
25
+ export const LICENSE_PUBKEY_FINGERPRINT_HEX = '18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475';
26
+
27
+ /** Allowlist of historically-trusted license pubkey fingerprints. */
28
+ export const KNOWN_LICENSE_PUBKEY_FINGERPRINTS: ReadonlySet<string> = new Set([
29
+ "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
30
+ ]);
@@ -0,0 +1,149 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * P-H019 (plan-stage-c-high-batch / 1.10.5): Ed25519 verifier for signed
6
+ * /validate-key responses from massu.ai.
7
+ *
8
+ * **Bug class closed**: pre-fix, `packages/core/src/license.ts:280-318`
9
+ * accepted ANY `{ valid: true, plan: 'enterprise' }` JSON over HTTPS from
10
+ * whatever endpoint `config.cloud?.endpoint` pointed to. MITM, malicious
11
+ * cloud.endpoint, or local edit of the cached `license_cache` SQLite row
12
+ * could grant arbitrary tier.
13
+ *
14
+ * **Structural fix**: the server (validate-key Supabase Edge Function)
15
+ * signs every response with Ed25519. The client (this module, called
16
+ * from `license.ts`) verifies the signature against the bundled public
17
+ * key in `license-pubkey.generated.ts`. Invalid / missing signature →
18
+ * reject under strict mode, warn-and-accept under transition mode.
19
+ *
20
+ * **Transition mode** (default): when `MASSU_REQUIRE_SIGNED_LICENSE` env
21
+ * var is NOT `'true'`, unsigned/invalid-sig responses are accepted but
22
+ * a one-shot stderr warning is emitted. Lets existing customers keep
23
+ * working while operators provision the signing key.
24
+ *
25
+ * **Strict mode** (post-cutover): when `MASSU_REQUIRE_SIGNED_LICENSE=true`,
26
+ * unsigned/invalid-sig responses are REJECTED (caller drops to grace-period
27
+ * cached or 'free' tier). Operator flips this after Supabase Edge Function
28
+ * env var `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` is provisioned and
29
+ * cutover smoke test passes.
30
+ */
31
+
32
+ import { createVerify, createPublicKey } from 'crypto';
33
+ import {
34
+ LICENSE_PUBKEY_ED25519,
35
+ LICENSE_PUBKEY_FINGERPRINT_HEX,
36
+ KNOWN_LICENSE_PUBKEY_FINGERPRINTS,
37
+ } from './license-pubkey.generated.ts';
38
+
39
+ export interface SignedLicenseResponse {
40
+ /** Base64-encoded Ed25519 signature over the canonical-serialized payload. */
41
+ _signature?: string;
42
+ /** Signature algorithm tag (currently always `'ed25519'`). */
43
+ _signature_alg?: string;
44
+ /**
45
+ * Sorted list of top-level keys (excluding the `_signature*` triplet) that
46
+ * were included in the signed payload. Lets the client reconstruct the
47
+ * canonical-serialization input deterministically.
48
+ */
49
+ _signature_payload_keys?: readonly string[];
50
+ /** Hex sha256 of the LICENSE pubkey used for signing (so client can detect rotation). */
51
+ _signature_pubkey_fingerprint?: string;
52
+ // Plus the actual fields (plan, tier, validUntil, features, etc.)
53
+ [key: string]: unknown;
54
+ }
55
+
56
+ export type VerificationResult =
57
+ | { kind: 'valid' }
58
+ | { kind: 'missing_signature' }
59
+ | { kind: 'bad_signature' }
60
+ | { kind: 'unknown_pubkey'; got: string }
61
+ | { kind: 'error'; reason: string };
62
+
63
+ /**
64
+ * Verify a server-signed /validate-key response. Returns a tagged union
65
+ * describing the verification outcome. Caller decides whether to accept
66
+ * based on strict/transition mode.
67
+ */
68
+ export function verifyLicenseResponse(payload: SignedLicenseResponse): VerificationResult {
69
+ // Self-check: confirm the bundled key fingerprint matches the allowlist.
70
+ // CR-9 audit L2 pattern: a future build that swaps the pem to an unknown
71
+ // key refuses to load even if compiled in.
72
+ if (!KNOWN_LICENSE_PUBKEY_FINGERPRINTS.has(LICENSE_PUBKEY_FINGERPRINT_HEX)) {
73
+ return {
74
+ kind: 'error',
75
+ reason: `Bundled license pubkey fingerprint ${LICENSE_PUBKEY_FINGERPRINT_HEX} is not in the trusted allowlist. Possible build-time tamper.`,
76
+ };
77
+ }
78
+
79
+ const sig = payload._signature;
80
+ const alg = payload._signature_alg;
81
+ const payloadKeys = payload._signature_payload_keys;
82
+ const sigPubkey = payload._signature_pubkey_fingerprint;
83
+
84
+ if (typeof sig !== 'string' || sig.length === 0) {
85
+ return { kind: 'missing_signature' };
86
+ }
87
+ if (alg !== 'ed25519') {
88
+ return { kind: 'error', reason: `Unsupported signature algorithm: ${alg}` };
89
+ }
90
+ if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
91
+ return { kind: 'error', reason: 'Missing _signature_payload_keys' };
92
+ }
93
+ if (typeof sigPubkey === 'string' && sigPubkey !== LICENSE_PUBKEY_FINGERPRINT_HEX) {
94
+ // Server signed with a different pubkey than we have bundled — rotation
95
+ // mismatch. Caller should fail-open under transition mode (operator
96
+ // hasn't published the new bundle yet) but reject under strict mode.
97
+ return { kind: 'unknown_pubkey', got: sigPubkey };
98
+ }
99
+
100
+ // Reconstruct the canonical serialization the server signed:
101
+ // JSON.stringify({ k1: payload[k1], k2: payload[k2], ... }, payloadKeysSorted)
102
+ // where payloadKeysSorted is the sorted list of NON-underscore-prefixed keys.
103
+ const canonicalObj: Record<string, unknown> = {};
104
+ for (const k of payloadKeys) {
105
+ if (typeof k !== 'string') continue;
106
+ canonicalObj[k] = payload[k];
107
+ }
108
+ const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
109
+
110
+ // Verify signature.
111
+ try {
112
+ // Construct a node KeyObject from the bundled raw Ed25519 pubkey bytes.
113
+ // We use createPublicKey with SubjectPublicKeyInfo DER encoding (44 bytes:
114
+ // 12-byte algorithm OID prefix + 32-byte raw key).
115
+ const spkiPrefix = Buffer.from([
116
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
117
+ 0x70, 0x03, 0x21, 0x00,
118
+ ]);
119
+ const der = Buffer.concat([spkiPrefix, Buffer.from(LICENSE_PUBKEY_ED25519)]);
120
+ const pubkey = createPublicKey({ key: der, format: 'der', type: 'spki' });
121
+
122
+ // Ed25519 in node uses `verify` directly (no createVerify needed for ed25519).
123
+ // crypto.verify(algorithm, data, key, signature) — algorithm MUST be null for Ed25519.
124
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
125
+ const { verify } = require('crypto');
126
+ const ok = verify(
127
+ null,
128
+ Buffer.from(canonical, 'utf-8'),
129
+ pubkey,
130
+ Buffer.from(sig, 'base64'),
131
+ );
132
+
133
+ return ok ? { kind: 'valid' } : { kind: 'bad_signature' };
134
+ } catch (err) {
135
+ return {
136
+ kind: 'error',
137
+ reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`,
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * P-H019 strict-mode gate. Operator sets `MASSU_REQUIRE_SIGNED_LICENSE=true`
144
+ * AFTER the validate-key Edge Function is provisioned with
145
+ * `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` and cutover smoke test passes.
146
+ */
147
+ export function isLicenseSignatureRequired(): boolean {
148
+ return process.env.MASSU_REQUIRE_SIGNED_LICENSE === 'true';
149
+ }
@@ -1,4 +1,4 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T05:57:44.313Z.
1
+ // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T15:41:04.323Z.
2
2
  // Source pem: packages/core/security/registry-pubkey.pem
3
3
  // RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
4
4
  // DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or