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 +1 -1
- package/package.json +1 -1
- package/src/cli/index.js +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/semantics.js +135 -7
- package/src/core/telemetry.js +1 -1
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +1 -1
- package/src/mcp/server.js +1 -1
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.
|
|
460
|
+
<p align="center"><i>v4.5.3 — 600+ 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.
|
|
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.
|
|
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]
|
package/src/core/compliance.js
CHANGED
package/src/core/semantics.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/src/core/telemetry.js
CHANGED
|
@@ -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.
|
|
260
|
+
version: "4.5.3",
|
|
261
261
|
totalCalls: summary.totalCalls,
|
|
262
262
|
avgResponseMs: summary.avgResponseMs,
|
|
263
263
|
conflicts: summary.conflicts,
|
package/src/dashboard/index.html
CHANGED
|
@@ -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.
|
|
92
|
+
<div class="meta">v4.5.3 — 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.
|
|
185
|
+
SpecLock v4.5.3 — Developed by Sandeep Roy — <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|
package/src/mcp/http-server.js
CHANGED
|
@@ -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.
|
|
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.
|
|
103
|
+
const VERSION = "4.5.3";
|
|
104
104
|
const AUTHOR = "Sandeep Roy";
|
|
105
105
|
|
|
106
106
|
const server = new McpServer(
|