@massu/core 1.10.3 → 1.10.5
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 +112 -9
- package/dist/hooks/classify-failure.js +1 -1
- package/dist/hooks/incident-pipeline.js +1 -1
- package/package.json +2 -2
- package/src/knowledge-indexer.ts +3 -3
- package/src/knowledge-tools.ts +4 -4
- package/src/license.ts +42 -0
- package/src/memory-db.ts +1 -1
- package/src/security/license-pubkey.generated.ts +30 -0
- package/src/security/license-response-verifier.ts +149 -0
- package/src/security/registry-pubkey.generated.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1605,7 +1605,7 @@ function addFailureClass(db, opts) {
|
|
|
1605
1605
|
return Number(result.lastInsertRowid);
|
|
1606
1606
|
}
|
|
1607
1607
|
function getFailureClasses(db) {
|
|
1608
|
-
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name").all();
|
|
1608
|
+
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name LIMIT 10000").all();
|
|
1609
1609
|
return rows.map((row) => ({
|
|
1610
1610
|
id: row.id,
|
|
1611
1611
|
name: row.name,
|
|
@@ -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,
|
|
@@ -25469,7 +25572,7 @@ function buildCrossReferences(db) {
|
|
|
25469
25572
|
const insertEdge = db.prepare(
|
|
25470
25573
|
"INSERT OR IGNORE INTO knowledge_edges (source_type, source_id, target_type, target_id, edge_type) VALUES (?, ?, ?, ?, ?)"
|
|
25471
25574
|
);
|
|
25472
|
-
const rules = db.prepare("SELECT rule_id, vr_type, reference_path FROM knowledge_rules").all();
|
|
25575
|
+
const rules = db.prepare("SELECT rule_id, vr_type, reference_path FROM knowledge_rules LIMIT 10000").all();
|
|
25473
25576
|
for (const rule of rules) {
|
|
25474
25577
|
if (rule.vr_type && rule.vr_type !== "VR-*") {
|
|
25475
25578
|
const vrTypes = rule.vr_type.split(/[,\s]+/).filter((v3) => v3.startsWith("VR-"));
|
|
@@ -25484,7 +25587,7 @@ function buildCrossReferences(db) {
|
|
|
25484
25587
|
edgeCount++;
|
|
25485
25588
|
}
|
|
25486
25589
|
}
|
|
25487
|
-
const incidents = db.prepare("SELECT incident_num, cr_added FROM knowledge_incidents WHERE cr_added IS NOT NULL").all();
|
|
25590
|
+
const incidents = db.prepare("SELECT incident_num, cr_added FROM knowledge_incidents WHERE cr_added IS NOT NULL LIMIT 10000").all();
|
|
25488
25591
|
for (const inc of incidents) {
|
|
25489
25592
|
if (inc.cr_added) {
|
|
25490
25593
|
const crIds = inc.cr_added.match(/CR-\d+/g) || [];
|
|
@@ -25494,7 +25597,7 @@ function buildCrossReferences(db) {
|
|
|
25494
25597
|
}
|
|
25495
25598
|
}
|
|
25496
25599
|
}
|
|
25497
|
-
const chunks = db.prepare("SELECT id, content, metadata FROM knowledge_chunks").all();
|
|
25600
|
+
const chunks = db.prepare("SELECT id, content, metadata FROM knowledge_chunks LIMIT 100000").all();
|
|
25498
25601
|
for (const chunk of chunks) {
|
|
25499
25602
|
const text18 = chunk.content;
|
|
25500
25603
|
const crRefs = text18.match(/CR-\d+/g);
|
|
@@ -26108,7 +26211,7 @@ function handleRule(db, args2) {
|
|
|
26108
26211
|
lines.push(`- **${rule.rule_id}**: ${rule.rule_text} | VR: ${rule.vr_type || "N/A"} | Ref: ${rule.reference_path || "N/A"}`);
|
|
26109
26212
|
}
|
|
26110
26213
|
} else {
|
|
26111
|
-
const rules = db.prepare("SELECT * FROM knowledge_rules ORDER BY rule_id").all();
|
|
26214
|
+
const rules = db.prepare("SELECT * FROM knowledge_rules ORDER BY rule_id LIMIT 10000").all();
|
|
26112
26215
|
lines.push(`## All Canonical Rules (${rules.length} total)`);
|
|
26113
26216
|
lines.push("");
|
|
26114
26217
|
for (const rule of rules) {
|
|
@@ -26217,7 +26320,7 @@ function handleSchemaCheck(db, args2) {
|
|
|
26217
26320
|
}
|
|
26218
26321
|
}
|
|
26219
26322
|
if (!table && !column) {
|
|
26220
|
-
const all = db.prepare("SELECT * FROM knowledge_schema_mismatches ORDER BY table_name").all();
|
|
26323
|
+
const all = db.prepare("SELECT * FROM knowledge_schema_mismatches ORDER BY table_name LIMIT 10000").all();
|
|
26221
26324
|
lines.push(`## All Known Schema Mismatches (${all.length} total)`);
|
|
26222
26325
|
lines.push("");
|
|
26223
26326
|
lines.push("| Table | WRONG | CORRECT |");
|
|
@@ -26355,7 +26458,7 @@ function handleVerification(db, args2) {
|
|
|
26355
26458
|
lines.push("");
|
|
26356
26459
|
}
|
|
26357
26460
|
} else {
|
|
26358
|
-
const all = db.prepare("SELECT vr_type, command, use_when FROM knowledge_verifications ORDER BY vr_type").all();
|
|
26461
|
+
const all = db.prepare("SELECT vr_type, command, use_when FROM knowledge_verifications ORDER BY vr_type LIMIT 10000").all();
|
|
26359
26462
|
lines.push(`## All Verification Types (${all.length} total)`);
|
|
26360
26463
|
lines.push("");
|
|
26361
26464
|
for (const vr of all) {
|
|
@@ -26743,7 +26846,7 @@ function handleEffectiveness2(db, args2) {
|
|
|
26743
26846
|
const topN = Math.min(args2.top_n || 10, 50);
|
|
26744
26847
|
const mode = args2.mode || "most_violated";
|
|
26745
26848
|
const lines = [];
|
|
26746
|
-
const allRules = db.prepare("SELECT rule_id, rule_text FROM knowledge_rules ORDER BY rule_id").all();
|
|
26849
|
+
const allRules = db.prepare("SELECT rule_id, rule_text FROM knowledge_rules ORDER BY rule_id LIMIT 10000").all();
|
|
26747
26850
|
let memoryDb = null;
|
|
26748
26851
|
const violationCounts = /* @__PURE__ */ new Map();
|
|
26749
26852
|
const lastViolated = /* @__PURE__ */ new Map();
|
|
@@ -1042,7 +1042,7 @@ function initMemorySchema(db) {
|
|
|
1042
1042
|
`);
|
|
1043
1043
|
}
|
|
1044
1044
|
function getFailureClasses(db) {
|
|
1045
|
-
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name").all();
|
|
1045
|
+
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name LIMIT 10000").all();
|
|
1046
1046
|
return rows.map((row) => ({
|
|
1047
1047
|
id: row.id,
|
|
1048
1048
|
name: row.name,
|
|
@@ -1059,7 +1059,7 @@ function addFailureClass(db, opts) {
|
|
|
1059
1059
|
return Number(result.lastInsertRowid);
|
|
1060
1060
|
}
|
|
1061
1061
|
function getFailureClasses(db) {
|
|
1062
|
-
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name").all();
|
|
1062
|
+
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name LIMIT 10000").all();
|
|
1063
1063
|
return rows.map((row) => ({
|
|
1064
1064
|
id: row.id,
|
|
1065
1065
|
name: row.name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
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/knowledge-indexer.ts
CHANGED
|
@@ -349,7 +349,7 @@ export function buildCrossReferences(db: Database.Database): number {
|
|
|
349
349
|
);
|
|
350
350
|
|
|
351
351
|
// CR -> VR edges (from knowledge_rules)
|
|
352
|
-
const rules = db.prepare('SELECT rule_id, vr_type, reference_path FROM knowledge_rules').all() as CRRule[];
|
|
352
|
+
const rules = db.prepare('SELECT rule_id, vr_type, reference_path FROM knowledge_rules LIMIT 10000').all() as CRRule[];
|
|
353
353
|
for (const rule of rules) {
|
|
354
354
|
if (rule.vr_type && rule.vr_type !== 'VR-*') {
|
|
355
355
|
// Split compound VR types (e.g., "VR-SCHEMA" or "VR-*")
|
|
@@ -367,7 +367,7 @@ export function buildCrossReferences(db: Database.Database): number {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
// Incident -> CR edges (from knowledge_incidents)
|
|
370
|
-
const incidents = db.prepare('SELECT incident_num, cr_added FROM knowledge_incidents WHERE cr_added IS NOT NULL').all() as { incident_num: number; cr_added: string }[];
|
|
370
|
+
const incidents = db.prepare('SELECT incident_num, cr_added FROM knowledge_incidents WHERE cr_added IS NOT NULL LIMIT 10000').all() as { incident_num: number; cr_added: string }[];
|
|
371
371
|
for (const inc of incidents) {
|
|
372
372
|
if (inc.cr_added) {
|
|
373
373
|
const crIds = inc.cr_added.match(/CR-\d+/g) || [];
|
|
@@ -379,7 +379,7 @@ export function buildCrossReferences(db: Database.Database): number {
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
// Scan all chunks for cross-references
|
|
382
|
-
const chunks = db.prepare('SELECT id, content, metadata FROM knowledge_chunks').all() as { id: number; content: string; metadata: string }[];
|
|
382
|
+
const chunks = db.prepare('SELECT id, content, metadata FROM knowledge_chunks LIMIT 100000').all() as { id: number; content: string; metadata: string }[];
|
|
383
383
|
for (const chunk of chunks) {
|
|
384
384
|
const text = chunk.content;
|
|
385
385
|
|
package/src/knowledge-tools.ts
CHANGED
|
@@ -462,7 +462,7 @@ function handleRule(db: Database.Database, args: Record<string, unknown>): ToolR
|
|
|
462
462
|
}
|
|
463
463
|
} else {
|
|
464
464
|
// List all rules
|
|
465
|
-
const rules = db.prepare('SELECT * FROM knowledge_rules ORDER BY rule_id').all() as {
|
|
465
|
+
const rules = db.prepare('SELECT * FROM knowledge_rules ORDER BY rule_id LIMIT 10000').all() as {
|
|
466
466
|
rule_id: string; rule_text: string; vr_type: string;
|
|
467
467
|
}[];
|
|
468
468
|
|
|
@@ -614,7 +614,7 @@ function handleSchemaCheck(db: Database.Database, args: Record<string, unknown>)
|
|
|
614
614
|
|
|
615
615
|
if (!table && !column) {
|
|
616
616
|
// Show all mismatches
|
|
617
|
-
const all = db.prepare('SELECT * FROM knowledge_schema_mismatches ORDER BY table_name').all() as {
|
|
617
|
+
const all = db.prepare('SELECT * FROM knowledge_schema_mismatches ORDER BY table_name LIMIT 10000').all() as {
|
|
618
618
|
table_name: string; wrong_column: string; correct_column: string;
|
|
619
619
|
}[];
|
|
620
620
|
|
|
@@ -804,7 +804,7 @@ function handleVerification(db: Database.Database, args: Record<string, unknown>
|
|
|
804
804
|
}
|
|
805
805
|
} else {
|
|
806
806
|
// List all
|
|
807
|
-
const all = db.prepare('SELECT vr_type, command, use_when FROM knowledge_verifications ORDER BY vr_type').all() as {
|
|
807
|
+
const all = db.prepare('SELECT vr_type, command, use_when FROM knowledge_verifications ORDER BY vr_type LIMIT 10000').all() as {
|
|
808
808
|
vr_type: string; command: string; use_when: string;
|
|
809
809
|
}[];
|
|
810
810
|
|
|
@@ -1319,7 +1319,7 @@ function handleEffectiveness(db: Database.Database, args: Record<string, unknown
|
|
|
1319
1319
|
const lines: string[] = [];
|
|
1320
1320
|
|
|
1321
1321
|
// Get all CR rules from knowledge DB
|
|
1322
|
-
const allRules = db.prepare('SELECT rule_id, rule_text FROM knowledge_rules ORDER BY rule_id').all() as {
|
|
1322
|
+
const allRules = db.prepare('SELECT rule_id, rule_text FROM knowledge_rules ORDER BY rule_id LIMIT 10000').all() as {
|
|
1323
1323
|
rule_id: string; rule_text: string;
|
|
1324
1324
|
}[];
|
|
1325
1325
|
|
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
|
package/src/memory-db.ts
CHANGED
|
@@ -1497,7 +1497,7 @@ export function addFailureClass(db: Database.Database, opts: AddFailureClassOpts
|
|
|
1497
1497
|
* Get all failure classes from the taxonomy.
|
|
1498
1498
|
*/
|
|
1499
1499
|
export function getFailureClasses(db: Database.Database): FailureClass[] {
|
|
1500
|
-
const rows = db.prepare('SELECT * FROM failure_classes ORDER BY name').all() as Array<Record<string, unknown>>;
|
|
1500
|
+
const rows = db.prepare('SELECT * FROM failure_classes ORDER BY name LIMIT 10000').all() as Array<Record<string, unknown>>;
|
|
1501
1501
|
return rows.map(row => ({
|
|
1502
1502
|
id: row.id as number,
|
|
1503
1503
|
name: row.name as string,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-license-pubkey.mjs at 2026-05-17T15:24:33.950Z.
|
|
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-
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T15:24:33.893Z.
|
|
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
|