@massu/core 1.13.0 → 1.14.0

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.
@@ -1049,6 +1049,45 @@ function initMemorySchema(db) {
1049
1049
  CREATE INDEX IF NOT EXISTS idx_so_file ON shared_observations(file_path);
1050
1050
  CREATE INDEX IF NOT EXISTS idx_so_module ON shared_observations(module);
1051
1051
  `);
1052
+ db.exec(`
1053
+ CREATE TABLE IF NOT EXISTS team_promotion_outbound (
1054
+ prompt_hash TEXT PRIMARY KEY,
1055
+ destination TEXT NOT NULL,
1056
+ draft_text TEXT NOT NULL,
1057
+ score REAL,
1058
+ signals_json TEXT NOT NULL DEFAULT '[]',
1059
+ content_hash TEXT NOT NULL,
1060
+ -- PA3-004 (Phase 3 Stream A): hardened-destination publish carries the
1061
+ -- publisher's review attestation so the server CHECK (hardened rows need a
1062
+ -- review_attestation) is satisfiable. hardened=0 for the Phase-2 rows.
1063
+ hardened INTEGER NOT NULL DEFAULT 0,
1064
+ review_attestation_json TEXT,
1065
+ created_at TEXT DEFAULT (datetime('now'))
1066
+ );
1067
+ CREATE TABLE IF NOT EXISTS team_revocation_outbound (
1068
+ prompt_hash TEXT PRIMARY KEY,
1069
+ created_at TEXT DEFAULT (datetime('now'))
1070
+ );
1071
+ `);
1072
+ db.exec(`
1073
+ CREATE TABLE IF NOT EXISTS rule_promotion_events_outbound (
1074
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1075
+ prompt_hash TEXT NOT NULL,
1076
+ event_type TEXT NOT NULL,
1077
+ metadata_json TEXT NOT NULL DEFAULT '{}',
1078
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1079
+ );
1080
+ CREATE INDEX IF NOT EXISTS idx_rpe_outbound_created ON rule_promotion_events_outbound(created_at);
1081
+ `);
1082
+ db.exec(`
1083
+ CREATE TABLE IF NOT EXISTS analytics_events (
1084
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1085
+ event_type TEXT NOT NULL,
1086
+ event_data TEXT NOT NULL DEFAULT '{}',
1087
+ created_at TEXT DEFAULT (datetime('now'))
1088
+ );
1089
+ CREATE INDEX IF NOT EXISTS idx_analytics_events_type ON analytics_events(event_type);
1090
+ `);
1052
1091
  db.exec(`
1053
1092
  CREATE TABLE IF NOT EXISTS knowledge_conflicts (
1054
1093
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1129,6 +1168,13 @@ function initMemorySchema(db) {
1129
1168
  `ALTER TABLE license_cache ADD COLUMN signed_payload_json TEXT NOT NULL DEFAULT ''`
1130
1169
  );
1131
1170
  }
1171
+ const outboundCols = db.prepare(`PRAGMA table_info(team_promotion_outbound)`).all();
1172
+ if (!outboundCols.some((c) => c.name === "hardened")) {
1173
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN hardened INTEGER NOT NULL DEFAULT 0`);
1174
+ }
1175
+ if (!outboundCols.some((c) => c.name === "review_attestation_json")) {
1176
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN review_attestation_json TEXT`);
1177
+ }
1132
1178
  db.exec(`
1133
1179
  CREATE TABLE IF NOT EXISTS failure_classes (
1134
1180
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1152,6 +1198,94 @@ function initMemorySchema(db) {
1152
1198
  function enqueueSyncPayload(db, payload) {
1153
1199
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
1154
1200
  }
1201
+ function getMemoryMeta(db, key) {
1202
+ const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
1203
+ return row ? row.value : null;
1204
+ }
1205
+ function setMemoryMeta(db, key, value) {
1206
+ db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
1207
+ }
1208
+ function drainTeamPromotions(db) {
1209
+ const rows = db.prepare(`
1210
+ SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
1211
+ FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
1212
+ `).all();
1213
+ if (rows.length === 0) return [];
1214
+ db.prepare("DELETE FROM team_promotion_outbound").run();
1215
+ return rows.map((r) => ({
1216
+ prompt_hash: r.prompt_hash,
1217
+ destination: r.destination,
1218
+ draft_text: r.draft_text,
1219
+ score: r.score ?? void 0,
1220
+ signals: safeJsonArray(r.signals_json),
1221
+ content_hash: r.content_hash,
1222
+ hardened: r.hardened === 1,
1223
+ review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
1224
+ }));
1225
+ }
1226
+ function drainTeamRevocations(db) {
1227
+ const rows = db.prepare(
1228
+ `SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
1229
+ ).all();
1230
+ if (rows.length === 0) return [];
1231
+ db.prepare("DELETE FROM team_revocation_outbound").run();
1232
+ return rows.map((r) => r.prompt_hash);
1233
+ }
1234
+ function getRecurrenceCountForPromptHash(db, promptHash) {
1235
+ try {
1236
+ const row = db.prepare(`
1237
+ SELECT json_extract(metadata, '$.recurrence_count') AS rc
1238
+ FROM audit_log
1239
+ WHERE event_type = 'rule_promoted'
1240
+ AND json_extract(metadata, '$.prompt_hash') = ?
1241
+ LIMIT 1
1242
+ `).get(promptHash);
1243
+ if (!row || row.rc === null || row.rc === void 0) return null;
1244
+ return Number(row.rc);
1245
+ } catch {
1246
+ return null;
1247
+ }
1248
+ }
1249
+ var FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
1250
+ function drainRulePromotionEvents(db) {
1251
+ const rows = db.prepare(`
1252
+ SELECT id, prompt_hash, event_type, metadata_json, created_at
1253
+ FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
1254
+ `).all(FUNNEL_EVENT_DRAIN_LIMIT);
1255
+ if (rows.length === 0) return [];
1256
+ const maxId = rows[rows.length - 1].id;
1257
+ db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
1258
+ return rows.map((r) => ({
1259
+ prompt_hash: r.prompt_hash,
1260
+ event_type: r.event_type,
1261
+ created_at: r.created_at,
1262
+ metadata: safeJsonParse(r.metadata_json) ?? {}
1263
+ }));
1264
+ }
1265
+ function recordTelemetry(db, eventType, data) {
1266
+ try {
1267
+ db.prepare(`
1268
+ INSERT INTO analytics_events (event_type, event_data, created_at)
1269
+ VALUES (?, ?, datetime('now'))
1270
+ `).run(eventType, JSON.stringify(data));
1271
+ } catch {
1272
+ }
1273
+ }
1274
+ function safeJsonArray(json) {
1275
+ try {
1276
+ const v = JSON.parse(json);
1277
+ return Array.isArray(v) ? v : [];
1278
+ } catch {
1279
+ return [];
1280
+ }
1281
+ }
1282
+ function safeJsonParse(json) {
1283
+ try {
1284
+ return JSON.parse(json);
1285
+ } catch {
1286
+ return void 0;
1287
+ }
1288
+ }
1155
1289
  function dequeuePendingSync(db, limit = 10) {
1156
1290
  const stale = db.prepare(
1157
1291
  "SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
@@ -1618,6 +1752,44 @@ async function syncToCloud(db, payload) {
1618
1752
  if (cloud.sync?.audit !== false) {
1619
1753
  filteredPayload.audit = payload.audit;
1620
1754
  }
1755
+ if (cloud.sync?.memory !== false) {
1756
+ if (payload.rule_promotions?.length) {
1757
+ let droppedPrivatePromos = 0;
1758
+ const safePromos = payload.rule_promotions.filter((p) => {
1759
+ if (classifyVisibility(p.draft_text ?? "", p.draft_text ?? "") === "private") {
1760
+ droppedPrivatePromos += 1;
1761
+ return false;
1762
+ }
1763
+ return true;
1764
+ });
1765
+ if (droppedPrivatePromos > 0) {
1766
+ process.stderr.write(
1767
+ `[massu] cloud-sync: dropped ${droppedPrivatePromos} team rule promotion(s) (PRIVATE_PATTERNS match in draft_text)
1768
+ `
1769
+ );
1770
+ }
1771
+ if (safePromos.length) filteredPayload.rule_promotions = safePromos;
1772
+ }
1773
+ if (payload.rule_revocations?.length) filteredPayload.rule_revocations = payload.rule_revocations;
1774
+ if (payload.rule_promotion_events?.length) {
1775
+ let droppedPrivateEvents = 0;
1776
+ const safeEvents = payload.rule_promotion_events.filter((e) => {
1777
+ const meta = e.metadata ? JSON.stringify(e.metadata) : "";
1778
+ if (meta && classifyVisibility(meta, meta) === "private") {
1779
+ droppedPrivateEvents += 1;
1780
+ return false;
1781
+ }
1782
+ return true;
1783
+ });
1784
+ if (droppedPrivateEvents > 0) {
1785
+ process.stderr.write(
1786
+ `[massu] cloud-sync: dropped ${droppedPrivateEvents} promotion funnel event(s) (PRIVATE_PATTERNS match in metadata)
1787
+ `
1788
+ );
1789
+ }
1790
+ if (safeEvents.length) filteredPayload.rule_promotion_events = safeEvents;
1791
+ }
1792
+ }
1621
1793
  let lastError = "";
1622
1794
  const requestTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
1623
1795
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -1698,6 +1870,496 @@ function sleep(ms) {
1698
1870
  return new Promise((resolve4) => setTimeout(resolve4, ms));
1699
1871
  }
1700
1872
 
1873
+ // src/team-rule-sync.ts
1874
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
1875
+ import { join, dirname as dirname4 } from "path";
1876
+
1877
+ // src/license.ts
1878
+ import { createHash } from "crypto";
1879
+
1880
+ // src/security/ed25519-envelope-verifier.ts
1881
+ import { createPublicKey, verify as cryptoVerify } from "crypto";
1882
+ var SPKI_ED25519_PREFIX = Buffer.from([
1883
+ 48,
1884
+ 42,
1885
+ 48,
1886
+ 5,
1887
+ 6,
1888
+ 3,
1889
+ 43,
1890
+ 101,
1891
+ 112,
1892
+ 3,
1893
+ 33,
1894
+ 0
1895
+ ]);
1896
+ function verifyEd25519SignedEnvelope(key, payload) {
1897
+ if (!key.knownFingerprints.has(key.fingerprintHex)) {
1898
+ return {
1899
+ kind: "error",
1900
+ reason: `Bundled ${key.keyLabel} pubkey fingerprint ${key.fingerprintHex} is not in the trusted allowlist. Possible build-time tamper.`
1901
+ };
1902
+ }
1903
+ const sig = payload._signature;
1904
+ const alg = payload._signature_alg;
1905
+ const payloadKeys = payload._signature_payload_keys;
1906
+ const sigPubkey = payload._signature_pubkey_fingerprint;
1907
+ if (typeof sig !== "string" || sig.length === 0) {
1908
+ return { kind: "missing_signature" };
1909
+ }
1910
+ if (alg !== "ed25519") {
1911
+ return { kind: "error", reason: `Unsupported signature algorithm: ${alg}` };
1912
+ }
1913
+ if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
1914
+ return { kind: "error", reason: "Missing _signature_payload_keys" };
1915
+ }
1916
+ if (typeof sigPubkey === "string" && sigPubkey !== key.fingerprintHex) {
1917
+ return { kind: "unknown_pubkey", got: sigPubkey };
1918
+ }
1919
+ const canonicalObj = {};
1920
+ for (const k of payloadKeys) {
1921
+ if (typeof k !== "string") continue;
1922
+ canonicalObj[k] = payload[k];
1923
+ }
1924
+ const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
1925
+ try {
1926
+ const der = Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(key.pubkeyBytes)]);
1927
+ const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
1928
+ const ok = cryptoVerify(
1929
+ null,
1930
+ Buffer.from(canonical, "utf-8"),
1931
+ pubkey,
1932
+ Buffer.from(sig, "base64")
1933
+ );
1934
+ return ok ? { kind: "valid" } : { kind: "bad_signature" };
1935
+ } catch (err) {
1936
+ return {
1937
+ kind: "error",
1938
+ reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`
1939
+ };
1940
+ }
1941
+ }
1942
+
1943
+ // src/security/license-pubkey.generated.ts
1944
+ var 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]);
1945
+ var LICENSE_PUBKEY_FINGERPRINT_HEX = "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475";
1946
+ var KNOWN_LICENSE_PUBKEY_FINGERPRINTS = /* @__PURE__ */ new Set([
1947
+ "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
1948
+ ]);
1949
+
1950
+ // src/security/license-response-verifier.ts
1951
+ function verifyLicenseResponse(payload) {
1952
+ return verifyEd25519SignedEnvelope(
1953
+ {
1954
+ pubkeyBytes: LICENSE_PUBKEY_ED25519,
1955
+ fingerprintHex: LICENSE_PUBKEY_FINGERPRINT_HEX,
1956
+ knownFingerprints: KNOWN_LICENSE_PUBKEY_FINGERPRINTS,
1957
+ keyLabel: "license"
1958
+ },
1959
+ payload
1960
+ );
1961
+ }
1962
+ function isLicenseSignatureRequired() {
1963
+ return process.env.MASSU_REQUIRE_SIGNED_LICENSE === "true";
1964
+ }
1965
+
1966
+ // src/license.ts
1967
+ var _warnedLicenseSig = false;
1968
+ function warnLicenseSigOnce(reason) {
1969
+ if (_warnedLicenseSig) return;
1970
+ _warnedLicenseSig = true;
1971
+ process.stderr.write(
1972
+ `[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.
1973
+ `
1974
+ );
1975
+ }
1976
+ var TIER_LEVELS = {
1977
+ free: 0,
1978
+ pro: 1,
1979
+ team: 2,
1980
+ enterprise: 3
1981
+ };
1982
+ function tierLevel(tier) {
1983
+ return TIER_LEVELS[tier] ?? 0;
1984
+ }
1985
+ var PLAN_TO_TIER_MAP = {
1986
+ free: "free",
1987
+ cloud_pro: "pro",
1988
+ cloud_team: "team",
1989
+ cloud_enterprise: "enterprise"
1990
+ };
1991
+ var IN_MEMORY_CACHE_TTL_MS = 15 * 60 * 1e3;
1992
+ var GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
1993
+ function readTrustedCache(cached) {
1994
+ if (!cached.signed_payload_json) {
1995
+ if (isLicenseSignatureRequired()) return null;
1996
+ warnLicenseSigOnce("cache_unsigned_transition");
1997
+ return {
1998
+ tier: cached.tier,
1999
+ validUntil: cached.valid_until,
2000
+ features: JSON.parse(cached.features || "[]")
2001
+ };
2002
+ }
2003
+ let parsed;
2004
+ try {
2005
+ parsed = JSON.parse(cached.signed_payload_json);
2006
+ } catch {
2007
+ return null;
2008
+ }
2009
+ const result = verifyLicenseResponse(parsed);
2010
+ if (result.kind !== "valid") return null;
2011
+ const verifiedPlan = typeof parsed.plan === "string" ? parsed.plan : null;
2012
+ const verifiedTierField = typeof parsed.tier === "string" ? parsed.tier : null;
2013
+ const tier = verifiedPlan ? PLAN_TO_TIER_MAP[verifiedPlan] ?? "free" : verifiedTierField ?? "free";
2014
+ const validUntil = typeof parsed.validUntil === "string" ? parsed.validUntil : "";
2015
+ const features = Array.isArray(parsed.features) ? parsed.features : [];
2016
+ const orgId = typeof parsed.orgId === "string" && parsed.orgId.length > 0 ? parsed.orgId : void 0;
2017
+ return { tier, validUntil, features, orgId };
2018
+ }
2019
+ function getCachedTierReadOnly(memDb) {
2020
+ const config = getConfig();
2021
+ const apiKey = config.cloud?.apiKey;
2022
+ if (!apiKey) return "free";
2023
+ const ownsDb = !memDb;
2024
+ const db = memDb ?? getMemoryDb();
2025
+ try {
2026
+ const keyHash = createHash("sha256").update(apiKey).digest("hex");
2027
+ const cached = db.prepare(
2028
+ "SELECT tier, valid_until, last_validated, features, signed_payload_json FROM license_cache WHERE api_key_hash = ?"
2029
+ ).get(keyHash);
2030
+ if (!cached) return "free";
2031
+ const trusted = readTrustedCache(cached);
2032
+ if (!trusted) return "free";
2033
+ const lastValidated = new Date(cached.last_validated);
2034
+ const sevenDaysAgo = new Date(Date.now() - GRACE_PERIOD_MS);
2035
+ if (!(lastValidated > sevenDaysAgo)) return "free";
2036
+ return trusted.tier;
2037
+ } catch {
2038
+ return "free";
2039
+ } finally {
2040
+ if (ownsDb) {
2041
+ try {
2042
+ db.close();
2043
+ } catch {
2044
+ }
2045
+ }
2046
+ }
2047
+ }
2048
+ function getCachedOrgId(memDb) {
2049
+ const config = getConfig();
2050
+ const apiKey = config.cloud?.apiKey;
2051
+ if (!apiKey) return null;
2052
+ const ownsDb = !memDb;
2053
+ const db = memDb ?? getMemoryDb();
2054
+ try {
2055
+ const keyHash = createHash("sha256").update(apiKey).digest("hex");
2056
+ const cached = db.prepare(
2057
+ "SELECT tier, valid_until, last_validated, features, signed_payload_json FROM license_cache WHERE api_key_hash = ?"
2058
+ ).get(keyHash);
2059
+ if (!cached) return null;
2060
+ const trusted = readTrustedCache(cached);
2061
+ if (!trusted) return null;
2062
+ const lastValidated = new Date(cached.last_validated);
2063
+ const sevenDaysAgo = new Date(Date.now() - GRACE_PERIOD_MS);
2064
+ if (!(lastValidated > sevenDaysAgo)) return null;
2065
+ return trusted.orgId ?? null;
2066
+ } catch {
2067
+ return null;
2068
+ } finally {
2069
+ if (ownsDb) {
2070
+ try {
2071
+ db.close();
2072
+ } catch {
2073
+ }
2074
+ }
2075
+ }
2076
+ }
2077
+
2078
+ // src/auto-learning-entitlement.ts
2079
+ var TEAM_SHARED_PROMOTION_MIN_TIER = "team";
2080
+ function entitledForTeamSharedPromotion(tier) {
2081
+ return tierLevel(tier) >= tierLevel(TEAM_SHARED_PROMOTION_MIN_TIER);
2082
+ }
2083
+
2084
+ // src/security/promotion-pubkey.generated.ts
2085
+ var PROMOTION_PUBKEY_ED25519 = new Uint8Array([107, 161, 33, 17, 189, 44, 193, 128, 252, 155, 188, 236, 100, 163, 23, 146, 219, 155, 216, 139, 134, 72, 211, 182, 151, 122, 209, 151, 135, 65, 167, 26]);
2086
+ var PROMOTION_PUBKEY_FINGERPRINT_HEX = "b14e2a73e23c02891e976ec161d339da6c930266c0202828d3187a3bd6e5d83f";
2087
+ var KNOWN_PROMOTION_PUBKEY_FINGERPRINTS = /* @__PURE__ */ new Set([
2088
+ "b14e2a73e23c02891e976ec161d339da6c930266c0202828d3187a3bd6e5d83f"
2089
+ ]);
2090
+
2091
+ // src/security/promotion-envelope-verifier.ts
2092
+ function verifyPromotionEnvelope(payload) {
2093
+ return verifyEd25519SignedEnvelope(
2094
+ {
2095
+ pubkeyBytes: PROMOTION_PUBKEY_ED25519,
2096
+ fingerprintHex: PROMOTION_PUBKEY_FINGERPRINT_HEX,
2097
+ knownFingerprints: KNOWN_PROMOTION_PUBKEY_FINGERPRINTS,
2098
+ keyLabel: "promotion"
2099
+ },
2100
+ payload
2101
+ );
2102
+ }
2103
+
2104
+ // src/rule-candidate-hardened.ts
2105
+ var TEAM_HARDENED_SHAREABLE_DESTINATIONS = [
2106
+ "pattern-scanner",
2107
+ "custom-destination"
2108
+ ];
2109
+ function isHardenedShareableDestination(destination) {
2110
+ return TEAM_HARDENED_SHAREABLE_DESTINATIONS.includes(destination);
2111
+ }
2112
+
2113
+ // src/rule-candidate-applier.ts
2114
+ var TEAM_SHAREABLE_DESTINATIONS = [
2115
+ "corrections-md",
2116
+ "claude-md-cr"
2117
+ ];
2118
+ function isTeamShareableDestination(destination) {
2119
+ return TEAM_SHAREABLE_DESTINATIONS.includes(destination);
2120
+ }
2121
+
2122
+ // src/team-knowledge.ts
2123
+ function shareObservation(db, developerId, project, observationType, summary, opts) {
2124
+ const result = db.prepare(`
2125
+ INSERT INTO shared_observations
2126
+ (original_id, developer_id, project, observation_type, summary, file_path, module, severity, is_shared, shared_at)
2127
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, datetime('now'))
2128
+ `).run(
2129
+ opts?.originalId ?? null,
2130
+ developerId,
2131
+ project,
2132
+ observationType,
2133
+ summary,
2134
+ opts?.filePath ?? null,
2135
+ opts?.module ?? null,
2136
+ opts?.severity ?? 3
2137
+ );
2138
+ return Number(result.lastInsertRowid);
2139
+ }
2140
+
2141
+ // src/team-rule-sync.ts
2142
+ var CURSOR_KEY = "team_promotions_cursor";
2143
+ var DEFAULT_TIMEOUT_MS = 2e3;
2144
+ var PROMPT_HASH_RE = /^[0-9a-f]{16}$/;
2145
+ var ZERO = {
2146
+ pulled: 0,
2147
+ materialized: 0,
2148
+ skipped: 0,
2149
+ dropped_unverified: 0,
2150
+ dropped_nonshareable: 0,
2151
+ revoked_handled: 0
2152
+ };
2153
+ async function pullTeamPromotions(db, opts = {}) {
2154
+ const config = getConfig();
2155
+ const cloud = config.cloud;
2156
+ const projectRoot = opts.projectRoot ?? getProjectRoot();
2157
+ const tier = opts.tier ?? getCachedTierReadOnly(db);
2158
+ if (!entitledForTeamSharedPromotion(tier)) return { ...ZERO };
2159
+ const endpoint = opts.endpoint ?? cloud?.endpoint;
2160
+ const apiKey = opts.apiKey ?? cloud?.apiKey;
2161
+ if (!endpoint || !apiKey) return { ...ZERO };
2162
+ const ownOrgId = opts.orgId !== void 0 ? opts.orgId : getCachedOrgId(db);
2163
+ const since = parseCursor(getMemoryMeta(db, CURSOR_KEY));
2164
+ const fetchImpl = opts.fetchImpl ?? fetch;
2165
+ const timeoutMs = opts.timeoutMs ?? cloud?.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
2166
+ let envelope;
2167
+ try {
2168
+ const res = await fetchImpl(`${endpoint}/promoted-rules?since=${since}`, {
2169
+ method: "GET",
2170
+ headers: { Authorization: `Bearer ${apiKey}` },
2171
+ signal: AbortSignal.timeout(timeoutMs)
2172
+ });
2173
+ if (!res.ok) return { ...ZERO };
2174
+ envelope = await res.json();
2175
+ } catch {
2176
+ return { ...ZERO };
2177
+ }
2178
+ const verdict = verifyPromotionEnvelope(envelope);
2179
+ if (verdict.kind !== "valid") {
2180
+ const result2 = { ...ZERO, dropped_unverified: countUntrusted(envelope) };
2181
+ emitDropTelemetry(db, "team_promotion_envelope_dropped", { reason: verdict.kind });
2182
+ return result2;
2183
+ }
2184
+ const signedKeys = Array.isArray(envelope._signature_payload_keys) ? envelope._signature_payload_keys : [];
2185
+ if (!signedKeys.includes("orgId") || !signedKeys.includes("promotions_json")) {
2186
+ const result2 = { ...ZERO, dropped_unverified: countUntrusted(envelope) };
2187
+ emitDropTelemetry(db, "team_promotion_unsigned_field", {
2188
+ orgId_signed: signedKeys.includes("orgId"),
2189
+ promotions_json_signed: signedKeys.includes("promotions_json")
2190
+ });
2191
+ return result2;
2192
+ }
2193
+ const signedOrgId = typeof envelope.orgId === "string" ? envelope.orgId : null;
2194
+ if (!signedOrgId || !ownOrgId || signedOrgId !== ownOrgId) {
2195
+ const result2 = { ...ZERO, dropped_unverified: countUntrusted(envelope) };
2196
+ emitDropTelemetry(db, "team_promotion_org_mismatch", {
2197
+ signed_org_present: !!signedOrgId,
2198
+ own_org_present: !!ownOrgId
2199
+ });
2200
+ return result2;
2201
+ }
2202
+ const promotions = parsePromotions(envelope.promotions_json);
2203
+ const result = { ...ZERO };
2204
+ let maxSeq = since;
2205
+ for (const p of promotions) {
2206
+ if (!isValidWirePromotion(p)) continue;
2207
+ result.pulled += 1;
2208
+ if (typeof p.seq === "number" && p.seq > maxSeq) maxSeq = p.seq;
2209
+ const hardenedMaterialize = isHardenedShareableDestination(p.destination) && p.hardened === true;
2210
+ if (!isTeamShareableDestination(p.destination) && !hardenedMaterialize) {
2211
+ result.dropped_nonshareable += 1;
2212
+ continue;
2213
+ }
2214
+ const candidatePath = sidecarPath(projectRoot, p.prompt_hash);
2215
+ if (p.revoked_at) {
2216
+ handleRevocation(db, projectRoot, candidatePath, p.prompt_hash);
2217
+ result.revoked_handled += 1;
2218
+ continue;
2219
+ }
2220
+ if (existsSync4(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
2221
+ result.skipped += 1;
2222
+ continue;
2223
+ }
2224
+ materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId);
2225
+ result.materialized += 1;
2226
+ }
2227
+ const serverCursor = typeof envelope.cursor === "number" ? envelope.cursor : 0;
2228
+ const nextCursor = Math.max(since, maxSeq, serverCursor);
2229
+ if (nextCursor > since) setMemoryMeta(db, CURSOR_KEY, String(nextCursor));
2230
+ return result;
2231
+ }
2232
+ function parseCursor(raw) {
2233
+ if (raw === null) return 0;
2234
+ const n = Number(raw);
2235
+ return Number.isInteger(n) && n >= 0 ? n : 0;
2236
+ }
2237
+ function parsePromotions(json) {
2238
+ if (typeof json !== "string") return [];
2239
+ try {
2240
+ const v = JSON.parse(json);
2241
+ return Array.isArray(v) ? v : [];
2242
+ } catch {
2243
+ return [];
2244
+ }
2245
+ }
2246
+ function countUntrusted(envelope) {
2247
+ const arr = parsePromotions(envelope.promotions_json);
2248
+ return arr.length > 0 ? arr.length : 1;
2249
+ }
2250
+ function isValidWirePromotion(p) {
2251
+ if (!p || typeof p !== "object") return false;
2252
+ const r = p;
2253
+ return typeof r.prompt_hash === "string" && PROMPT_HASH_RE.test(r.prompt_hash) && typeof r.destination === "string" && typeof r.draft_text === "string" && typeof r.promoted_by === "string" && typeof r.promoted_at === "string";
2254
+ }
2255
+ function sidecarPath(projectRoot, promptHash) {
2256
+ return join(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
2257
+ }
2258
+ function alreadyApplied(db, promptHash) {
2259
+ try {
2260
+ const row = db.prepare(
2261
+ `SELECT 1 FROM audit_log WHERE event_type = 'rule_promoted'
2262
+ AND json_extract(metadata, '$.prompt_hash') = ? LIMIT 1`
2263
+ ).get(promptHash);
2264
+ return !!row;
2265
+ } catch {
2266
+ return false;
2267
+ }
2268
+ }
2269
+ function handleRevocation(db, projectRoot, candidatePath, promptHash) {
2270
+ if (existsSync4(candidatePath)) {
2271
+ try {
2272
+ unlinkSync(candidatePath);
2273
+ } catch {
2274
+ }
2275
+ return;
2276
+ }
2277
+ if (alreadyApplied(db, promptHash)) {
2278
+ process.stderr.write(
2279
+ `[massu] team rule ${promptHash} was revoked by your org \u2014 consider reverting it.
2280
+ `
2281
+ );
2282
+ }
2283
+ }
2284
+ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
2285
+ const promptText = p.draft_text.replace(/\n+/g, " ").slice(0, 200) || `team rule ${p.prompt_hash}`;
2286
+ const sidecar = {
2287
+ // Standard RuleCandidatePayload fields (so `/massu-rule approve` → readCandidate
2288
+ // → validateCandidatePayload passes), synthesized from the promotion.
2289
+ prompt: promptText,
2290
+ prompt_hash: p.prompt_hash,
2291
+ score: clampScore(p.score),
2292
+ signals: sanitizeSignals(p.signals),
2293
+ prior_turn_files: [],
2294
+ timestamp: p.promoted_at,
2295
+ session_id: `team:${p.promoted_by}`,
2296
+ // Provenance (PB-004): the applier's team-origin gate keys on this. PA3-005:
2297
+ // a hardened materialization sets `hardened: true` so the applier's hardened
2298
+ // apply-gate (PA3-004) engages. `review_attestation` is intentionally NOT
2299
+ // copied from the publisher here — the RECEIVER's `/massu-rule review` records
2300
+ // ITS OWN two-operator + render-only ack into provenance.review_attestation
2301
+ // before apply; until then the gate refuses (hardened-PENDING).
2302
+ provenance: {
2303
+ origin: "team",
2304
+ org_id: orgId,
2305
+ promoted_by: p.promoted_by,
2306
+ promoted_at: p.promoted_at,
2307
+ signature_verified: true,
2308
+ ...p.hardened === true ? { hardened: true } : {}
2309
+ },
2310
+ // Extra fields the `/massu-rule approve` flow reads to drive the apply (the
2311
+ // publisher already decided destination + body). validateCandidatePayload
2312
+ // ignores unknown keys. `publisher_review_attestation` is retained for the
2313
+ // receiver's review UI (display only — never the apply-gate authority).
2314
+ destination: p.destination,
2315
+ draft_text: p.draft_text,
2316
+ ...p.review_attestation !== void 0 ? { publisher_review_attestation: p.review_attestation } : {}
2317
+ };
2318
+ const dir = dirname4(candidatePath);
2319
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
2320
+ writeFileSync2(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
2321
+ try {
2322
+ shareObservation(db, p.promoted_by, getProjectName(), "rule_promotion", promptText, {
2323
+ filePath: void 0,
2324
+ module: p.destination
2325
+ });
2326
+ } catch {
2327
+ }
2328
+ }
2329
+ function clampScore(score) {
2330
+ if (typeof score !== "number" || !Number.isFinite(score)) return 0;
2331
+ return Math.max(-200, Math.min(200, score));
2332
+ }
2333
+ function sanitizeSignals(signals) {
2334
+ if (!Array.isArray(signals)) return [];
2335
+ const out = [];
2336
+ for (const s of signals) {
2337
+ if (!s || typeof s !== "object") continue;
2338
+ const sig = s;
2339
+ out.push({
2340
+ name: typeof sig.name === "string" ? sig.name : "unknown",
2341
+ baseWeight: typeof sig.baseWeight === "number" ? sig.baseWeight : 0,
2342
+ applied: typeof sig.applied === "number" ? sig.applied : 0,
2343
+ ...typeof sig.evidence === "string" ? { evidence: sig.evidence } : {}
2344
+ });
2345
+ }
2346
+ return out;
2347
+ }
2348
+ function getProjectName() {
2349
+ try {
2350
+ return getConfig().project?.name ?? "massu";
2351
+ } catch {
2352
+ return "massu";
2353
+ }
2354
+ }
2355
+ function emitDropTelemetry(db, eventType, data) {
2356
+ recordTelemetry(db, eventType, data);
2357
+ process.stderr.write(
2358
+ `[massu] team-shared promotion pull: dropped envelope (${eventType}). A signed/org-matched response is required \u2014 see massu.ai for details.
2359
+ `
2360
+ );
2361
+ }
2362
+
1701
2363
  // src/analytics.ts
1702
2364
  var DEFAULT_WEIGHTS = {
1703
2365
  bug_found: -5,
@@ -1843,7 +2505,7 @@ function storeSessionCost(db, sessionId, usage, cost) {
1843
2505
  }
1844
2506
 
1845
2507
  // src/prompt-analyzer.ts
1846
- import { createHash } from "crypto";
2508
+ import { createHash as createHash2 } from "crypto";
1847
2509
 
1848
2510
  // src/security-utils.ts
1849
2511
  function escapeRegex(str) {
@@ -1867,7 +2529,7 @@ function categorizePrompt(promptText) {
1867
2529
  }
1868
2530
  function hashPrompt(promptText) {
1869
2531
  const normalized = promptText.toLowerCase().replace(/\s+/g, " ").trim();
1870
- return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
2532
+ return createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
1871
2533
  }
1872
2534
  function detectOutcome(followUpPrompts, assistantResponses) {
1873
2535
  let correctionsNeeded = 0;
@@ -1991,10 +2653,14 @@ async function main() {
1991
2653
  archiveAndRegenerate(db, session_id);
1992
2654
  try {
1993
2655
  await drainSyncQueue(db);
1994
- const syncPayload = buildSyncPayload(session_id, observations, summary);
2656
+ const syncPayload = buildSyncPayload(db, session_id, observations, summary);
1995
2657
  const result = await syncToCloud(db, syncPayload);
1996
2658
  if (!result.success && result.error) {
1997
2659
  }
2660
+ try {
2661
+ await pullTeamPromotions(db);
2662
+ } catch (_pullErr) {
2663
+ }
1998
2664
  } catch (_syncErr) {
1999
2665
  }
2000
2666
  } finally {
@@ -2004,7 +2670,12 @@ async function main() {
2004
2670
  }
2005
2671
  process.exit(0);
2006
2672
  }
2007
- function buildSyncPayload(sessionId, observations, summary) {
2673
+ function buildSyncPayload(db, sessionId, observations, summary) {
2674
+ const cfg = getConfig().cloud;
2675
+ const willTransmit = !!cfg?.enabled && !!cfg?.apiKey && cfg?.sync?.memory !== false;
2676
+ const promotions = willTransmit ? drainTeamPromotions(db) : [];
2677
+ const revocations = willTransmit ? drainTeamRevocations(db) : [];
2678
+ const funnelEvents = willTransmit ? drainRulePromotionEvents(db) : [];
2008
2679
  return {
2009
2680
  sessions: [{
2010
2681
  local_session_id: sessionId,
@@ -2024,7 +2695,33 @@ function buildSyncPayload(sessionId, observations, summary) {
2024
2695
  content: o.title + (o.detail ? `: ${o.detail}` : ""),
2025
2696
  importance: o.importance ?? 3,
2026
2697
  file_path: void 0
2027
- }))
2698
+ })),
2699
+ ...promotions.length > 0 ? {
2700
+ rule_promotions: promotions.map((p) => {
2701
+ const recurrence = getRecurrenceCountForPromptHash(db, p.prompt_hash);
2702
+ return {
2703
+ prompt_hash: p.prompt_hash,
2704
+ destination: p.destination,
2705
+ draft_text: p.draft_text,
2706
+ score: p.score,
2707
+ signals: p.signals,
2708
+ content_hash: p.content_hash,
2709
+ ...recurrence !== null ? { recurrence_count: recurrence } : {},
2710
+ // PA3-004: hardened-destination publish carries the flag + attestation.
2711
+ ...p.hardened ? { hardened: true } : {},
2712
+ ...p.review_attestation !== void 0 ? { review_attestation: p.review_attestation } : {}
2713
+ };
2714
+ })
2715
+ } : {},
2716
+ ...revocations.length > 0 ? { rule_revocations: revocations.map((prompt_hash) => ({ prompt_hash })) } : {},
2717
+ ...funnelEvents.length > 0 ? {
2718
+ rule_promotion_events: funnelEvents.map((e) => ({
2719
+ prompt_hash: e.prompt_hash,
2720
+ event_type: e.event_type,
2721
+ created_at: e.created_at,
2722
+ ...e.metadata && Object.keys(e.metadata).length > 0 ? { metadata: e.metadata } : {}
2723
+ }))
2724
+ } : {}
2028
2725
  };
2029
2726
  }
2030
2727
  function buildSummaryFromObservations(observations, prompts) {