speclock 4.5.2 → 4.5.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/README.md CHANGED
@@ -457,4 +457,4 @@ Built by **[Sandeep Roy](https://github.com/sgroy10)**
457
457
 
458
458
  ---
459
459
 
460
- <p align="center"><i>v4.4.260 tests, 31 MCP tools, 0 false positives, Gemini hybrid. Because remembering isn't enough.</i></p>
460
+ <p align="center"><i>v4.5.3600+ tests, 31 MCP tools, 0 false positives, Gemini hybrid. Because remembering isn't enough.</i></p>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  "name": "speclock",
4
4
 
5
- "version": "4.5.2",
5
+ "version": "4.5.3",
6
6
 
7
7
  "description": "AI constraint engine with Gemini LLM universal detection, Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. Cross-platform: MCP + direct API. 31 MCP tools + CLI. Enterprise platform.",
8
8
 
package/src/cli/index.js CHANGED
@@ -117,7 +117,7 @@ function refreshContext(root) {
117
117
 
118
118
  function printHelp() {
119
119
  console.log(`
120
- SpecLock v4.5.2 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
120
+ SpecLock v4.5.3 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
121
121
  Developed by Sandeep Roy (github.com/sgroy10)
122
122
 
123
123
  Usage: speclock <command> [options]
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "4.5.2";
12
+ const VERSION = "4.5.3";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -82,6 +82,16 @@ export const SYNONYM_GROUPS = [
82
82
  ["audit", "audit log", "audit trail", "logging", "log",
83
83
  "monitoring", "observability", "telemetry", "tracking"],
84
84
 
85
+ // --- Auth providers ---
86
+ ["auth0", "okta", "cognito", "keycloak", "supabase auth"],
87
+
88
+ // --- API keys & secrets ---
89
+ ["api key", "api keys", "secret key", "secret keys", "publishable key",
90
+ "private key", "access key", "api secret", "api token",
91
+ "credentials", "credential"],
92
+ ["frontend", "frontend code", "client-side", "client side",
93
+ "browser", "react state", "ui component"],
94
+
85
95
  // --- Dependencies ---
86
96
  ["dependency", "package", "library", "module", "import", "require",
87
97
  "vendor", "third-party"],
@@ -619,6 +629,30 @@ export const CONCEPT_MAP = {
619
629
  "saml": ["sso", "oidc", "single sign-on", "authentication",
620
630
  "identity provider"],
621
631
 
632
+ // API keys & secrets
633
+ "api key": ["api keys", "secret key", "api secret", "api token",
634
+ "credential", "publishable key", "access key",
635
+ "secret", "key", "frontend code", "expose"],
636
+ "api keys": ["api key", "secret key", "api secret", "credential",
637
+ "publishable key", "access key", "frontend code", "expose"],
638
+ "secret key": ["api key", "api secret", "credential", "secret",
639
+ "publishable key", "private key"],
640
+ "publishable key": ["api key", "public key", "stripe key", "client key",
641
+ "frontend", "credential"],
642
+ "secret": ["api key", "secret key", "credential", "api secret",
643
+ "private", "sensitive"],
644
+ "localstorage": ["client-side storage", "browser storage", "frontend",
645
+ "expose", "client-side"],
646
+ // Auth providers
647
+ "auth0": ["authentication", "auth", "identity provider", "sso",
648
+ "oauth", "supabase", "cognito", "okta", "keycloak"],
649
+ "cognito": ["authentication", "auth", "identity provider",
650
+ "auth0", "supabase", "okta"],
651
+ "okta": ["authentication", "auth", "identity provider", "sso",
652
+ "auth0", "supabase", "cognito"],
653
+ "keycloak": ["authentication", "auth", "identity provider", "sso",
654
+ "auth0", "supabase"],
655
+
622
656
  // Networking
623
657
  "websocket": ["socket", "real-time connection", "ws", "wss",
624
658
  "socket connection"],
@@ -847,7 +881,16 @@ function buildKnownPhrases() {
847
881
  const KNOWN_PHRASES = buildKnownPhrases();
848
882
 
849
883
  export function tokenize(text) {
850
- const lower = text.toLowerCase();
884
+ // Pre-process: split UPPER_CASE_ENV_VARS into component words
885
+ // "STRIPE_SECRET_KEY" → "STRIPE SECRET KEY"
886
+ // "process.env.STRIPE_KEY" → "process env STRIPE KEY"
887
+ let preprocessed = text.replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, match =>
888
+ match.replace(/_/g, " ")
889
+ );
890
+ // Also handle process.env.X patterns
891
+ preprocessed = preprocessed.replace(/process\.env\./gi, "env ");
892
+
893
+ const lower = preprocessed.toLowerCase();
851
894
  const phrases = [];
852
895
 
853
896
  // Extract known multi-word phrases (greedy, longest first)
@@ -1167,6 +1210,7 @@ const NEUTRAL_ACTION_VERBS = [
1167
1210
  "replace", "swap", "switch", "migrate", "transition", "substitute",
1168
1211
  "touch", "mess", "configure", "optimize", "tweak",
1169
1212
  "extend", "shorten", "adjust", "customize", "personalize",
1213
+ "update", "rewrite",
1170
1214
  ];
1171
1215
 
1172
1216
  function extractPrimaryVerb(actionText) {
@@ -1605,6 +1649,7 @@ export function scoreConflict({ actionText, lockText }) {
1605
1649
 
1606
1650
  let score = 0;
1607
1651
  const reasons = [];
1652
+ let hasSecurityViolationPattern = false; // Set when credential-exposure detected
1608
1653
 
1609
1654
  // 1. Direct word overlap (minus stopwords)
1610
1655
  const directOverlap = actionTokens.words.filter(w =>
@@ -1906,6 +1951,24 @@ export function scoreConflict({ actionText, lockText }) {
1906
1951
  }
1907
1952
  }
1908
1953
 
1954
+ // Special case: "add/put/store/embed key/secret/credential in/to frontend/component/state"
1955
+ // is SEMANTICALLY EQUIVALENT to "expose key in frontend" — NOT an opposite action.
1956
+ // Placing credentials in client-side code IS exposing them.
1957
+ if (!actionPerformsProhibitedOp && (prohibitedVerb === "expose" || prohibitedVerb === "leak" || prohibitedVerb === "reveal")) {
1958
+ // Pre-process env vars: STRIPE_SECRET_KEY → stripe secret key
1959
+ const actionNorm = actionText
1960
+ .replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, m => m.replace(/_/g, " "))
1961
+ .toLowerCase();
1962
+ const hasCredentialWord = /\b(key|keys|secret|secrets|credential|credentials|token|api.?key|password|cert)\b/i.test(actionNorm);
1963
+ const hasFrontendLocation = /\b(frontend|front.?end|client|component|state|localstorage|session.?storage|browser|ui|react|vue|angular|svelte|html|template)\b/i.test(actionNorm);
1964
+ const hasPlacementVerb = /\b(add|put|store|embed|include|place|insert|set|hardcode|inline)\b/i.test(actionNorm);
1965
+ if (hasCredentialWord && hasFrontendLocation && hasPlacementVerb) {
1966
+ actionPerformsProhibitedOp = true;
1967
+ hasSecurityViolationPattern = true;
1968
+ reasons.push("security: placing credentials in client-side code is equivalent to exposing them");
1969
+ }
1970
+ }
1971
+
1909
1972
  if (prohibitedIsNegative && !actionIntent.negated &&
1910
1973
  !hasDestructiveLanguageMatch && !actionPerformsProhibitedOp) {
1911
1974
  intentAligned = true;
@@ -1974,10 +2037,10 @@ export function scoreConflict({ actionText, lockText }) {
1974
2037
  // Check 3b: Safe/verification verbs against preservation/maintenance locks
1975
2038
  // "Test that Stripe is working" is COMPLIANT with "must always use Stripe"
1976
2039
  // "Debug the Stripe webhook" is COMPLIANT — it's verifying the preserved system
1977
- if (!intentAligned && actionPrimaryVerb) {
2040
+ {
1978
2041
  const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay/i.test(lockText);
1979
2042
 
1980
- if (lockIsPreservation) {
2043
+ if (!intentAligned && lockIsPreservation) {
1981
2044
  const SAFE_FOR_PRESERVATION = new Set([
1982
2045
  "test", "verify", "check", "validate", "confirm", "ensure",
1983
2046
  "debug", "inspect", "review", "examine", "monitor", "observe",
@@ -1985,12 +2048,76 @@ export function scoreConflict({ actionText, lockText }) {
1985
2048
  "read", "view", "generate", "fix", "repair", "patch",
1986
2049
  "protect", "secure", "guard", "maintain", "preserve",
1987
2050
  ]);
1988
- if (SAFE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
2051
+ if (actionPrimaryVerb && SAFE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
1989
2052
  intentAligned = true;
1990
2053
  reasons.push(
1991
2054
  `intent alignment: verification/maintenance "${actionPrimaryVerb}" is ` +
1992
2055
  `compliant with preservation lock`);
1993
2056
  }
2057
+ // "Write tests for X" — the verb is "write" but the intent is testing
2058
+ // Uses raw text match since "write" may not be in NEUTRAL_ACTION_VERBS
2059
+ if (!intentAligned && /\bwrite\s+tests?\b/i.test(actionText)) {
2060
+ intentAligned = true;
2061
+ reasons.push(
2062
+ `intent alignment: "write tests" is a testing/verification action — ` +
2063
+ `compliant with preservation lock`);
2064
+ }
2065
+ }
2066
+ }
2067
+
2068
+ // Check 3c: Working WITH locked technology (not replacing it)
2069
+ // "Update the Stripe UI components" vs "must always use Stripe" → working WITH Stripe → safe
2070
+ // "Optimize Supabase queries" vs "Supabase Auth lock" → improving existing Supabase → safe
2071
+ // "Write tests for Supabase queries" vs "Supabase lock" → testing existing tech → safe
2072
+ // But: "Update payment to use Razorpay" vs "Stripe lock" → introducing competitor → NOT safe
2073
+ if (!intentAligned && actionPrimaryVerb) {
2074
+ const lockIsPreservationOrFreeze = /must remain|must be preserved|must always|at all times|must stay|do not replace|do not remove|do not switch|don't replace|don't remove|don't switch|uses .+ library/i.test(lockText);
2075
+ if (lockIsPreservationOrFreeze) {
2076
+ // Extract specific brand/tech names from the lock text
2077
+ const lockWords = lockText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
2078
+ const actionWords = actionText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
2079
+
2080
+ // Find brand/technology names that appear in BOTH action and lock
2081
+ // These are specific nouns (not verbs, not stopwords) that identify the technology
2082
+ const TECH_BRANDS = new Set([
2083
+ "stripe", "razorpay", "paypal", "phonepe", "paytm", "ccavenue", "cashfree",
2084
+ "braintree", "adyen", "square", "billdesk", "instamojo", "juspay",
2085
+ "postgresql", "postgres", "mysql", "mongodb", "mongo", "firebase",
2086
+ "firestore", "supabase", "dynamodb", "redis", "sqlite", "mariadb",
2087
+ "cassandra", "couchdb", "neo4j",
2088
+ "baileys", "twilio", "whatsapp",
2089
+ "auth0", "okta", "cognito", "keycloak",
2090
+ "react", "vue", "angular", "svelte", "nextjs", "nuxt",
2091
+ "docker", "kubernetes", "terraform", "ansible",
2092
+ "aws", "gcp", "azure", "vercel", "netlify", "railway", "heroku",
2093
+ ]);
2094
+ const sharedBrands = lockWords.filter(w => TECH_BRANDS.has(w) && actionWords.includes(w));
2095
+
2096
+ if (sharedBrands.length > 0) {
2097
+ // Action references the SAME tech as the lock — check if it's working WITH it
2098
+ const hasCompetitorInAction = actionWords.some(w =>
2099
+ TECH_BRANDS.has(w) && !lockWords.includes(w)
2100
+ );
2101
+ const WORKING_WITH_VERBS = new Set([
2102
+ "update", "modify", "change", "refactor", "restructure",
2103
+ "optimize", "improve", "enhance", "write", "rewrite",
2104
+ "style", "format", "clean", "cleanup", "simplify",
2105
+ "test", "verify", "check", "validate", "debug", "fix",
2106
+ "repair", "patch", "maintain", "document", "monitor",
2107
+ "configure", "customize", "extend", "expand",
2108
+ ]);
2109
+ const DESTRUCTIVE_VERBS = new Set([
2110
+ "remove", "delete", "drop", "destroy", "kill", "purge", "wipe",
2111
+ "disable", "deactivate", "replace", "switch", "migrate", "move",
2112
+ ]);
2113
+ if (WORKING_WITH_VERBS.has(actionPrimaryVerb) && !hasCompetitorInAction &&
2114
+ !DESTRUCTIVE_VERBS.has(actionPrimaryVerb)) {
2115
+ intentAligned = true;
2116
+ reasons.push(
2117
+ `intent alignment: "${actionPrimaryVerb}" works WITH locked tech ` +
2118
+ `${sharedBrands.join(", ")} — not replacing it`);
2119
+ }
2120
+ }
1994
2121
  }
1995
2122
  }
1996
2123
 
@@ -2002,7 +2129,7 @@ export function scoreConflict({ actionText, lockText }) {
2002
2129
  if (!intentAligned && actionPrimaryVerb) {
2003
2130
  const ENHANCEMENT_VERBS = new Set([
2004
2131
  "increase", "improve", "enhance", "boost", "strengthen",
2005
- "upgrade", "raise", "expand", "extend", "grow",
2132
+ "upgrade", "raise", "expand", "extend", "grow", "optimize",
2006
2133
  ]);
2007
2134
  const CONSTRUCTIVE_FOR_PRESERVATION = new Set([
2008
2135
  "build", "add", "create", "implement", "make", "design",
@@ -2049,7 +2176,8 @@ export function scoreConflict({ actionText, lockText }) {
2049
2176
  "integrate", "include", "support",
2050
2177
  ]);
2051
2178
  // 5a: Weak scope overlap (single shared word, no strong vocab match)
2052
- if (!intentAligned && hasScopeMatch && !hasStrongScopeMatch && !hasStrongVocabMatch) {
2179
+ // Skip if a security violation pattern was detected (credential exposure)
2180
+ if (!intentAligned && !hasSecurityViolationPattern && hasScopeMatch && !hasStrongScopeMatch && !hasStrongVocabMatch) {
2053
2181
  if (FEATURE_BUILDING_VERBS.has(actionPrimaryVerb)) {
2054
2182
  // Guard: if the shared word is a long, specific entity name (10+ chars),
2055
2183
  // it strongly identifies the target system — not an incidental overlap.
@@ -2066,7 +2194,7 @@ export function scoreConflict({ actionText, lockText }) {
2066
2194
  }
2067
2195
  // 5b: Vocab overlap but NO scope overlap (subjects point to different things)
2068
2196
  // Even weaker signal — shared vocabulary but different actual components.
2069
- if (!intentAligned && hasVocabSubjectMatch && !hasScopeMatch &&
2197
+ if (!intentAligned && !hasSecurityViolationPattern && hasVocabSubjectMatch && !hasScopeMatch &&
2070
2198
  subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
2071
2199
  if (FEATURE_BUILDING_VERBS.has(actionPrimaryVerb)) {
2072
2200
  intentAligned = true;
@@ -257,7 +257,7 @@ export async function flushToRemote(root) {
257
257
  // Build anonymized payload
258
258
  const payload = {
259
259
  instanceId: summary.instanceId,
260
- version: "4.5.2",
260
+ version: "4.5.3",
261
261
  totalCalls: summary.totalCalls,
262
262
  avgResponseMs: summary.avgResponseMs,
263
263
  conflicts: summary.conflicts,
@@ -89,7 +89,7 @@
89
89
  <div class="header">
90
90
  <div>
91
91
  <h1><span>SpecLock</span> Dashboard</h1>
92
- <div class="meta">v4.5.2 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v4.5.3 &mdash; AI Constraint Engine</div>
93
93
  </div>
94
94
  <div style="display:flex;align-items:center;gap:12px;">
95
95
  <span id="health-badge" class="status-badge healthy">Loading...</span>
@@ -182,7 +182,7 @@
182
182
  </div>
183
183
 
184
184
  <div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
185
- SpecLock v4.5.2 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v4.5.3 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
186
186
  </div>
187
187
 
188
188
  <script>
@@ -91,7 +91,7 @@ import { fileURLToPath } from "url";
91
91
  import _path from "path";
92
92
 
93
93
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
94
- const VERSION = "4.5.2";
94
+ const VERSION = "4.5.3";
95
95
  const AUTHOR = "Sandeep Roy";
96
96
  const START_TIME = Date.now();
97
97
 
package/src/mcp/server.js CHANGED
@@ -100,7 +100,7 @@ const PROJECT_ROOT =
100
100
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
101
101
 
102
102
  // --- MCP Server ---
103
- const VERSION = "4.5.2";
103
+ const VERSION = "4.5.3";
104
104
  const AUTHOR = "Sandeep Roy";
105
105
 
106
106
  const server = new McpServer(