@equilateral_ai/mindmeld 3.5.0 → 3.5.1
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/hooks/pre-compact.js +269 -21
- package/hooks/session-start.js +139 -34
- package/package.json +1 -1
- package/scripts/auth-login.js +45 -8
- package/src/core/StandardsIngestion.js +3 -1
- package/src/handlers/collaborators/collaboratorList.js +4 -10
- package/src/handlers/correlations/correlationsProjectGet.js +4 -13
- package/src/handlers/github/githubDiscoverPatterns.js +4 -8
- package/src/handlers/github/githubPatternsReview.js +4 -8
- package/src/handlers/helpers/decisionFrames.js +29 -0
- package/src/handlers/helpers/index.js +14 -0
- package/src/handlers/helpers/mindmeldMcpCore.js +566 -57
- package/src/handlers/helpers/predictiveCache.js +51 -0
- package/src/handlers/helpers/projectAccess.js +88 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +113 -14
- package/src/handlers/standards/discoveriesGet.js +4 -8
- package/src/handlers/standards/projectStandardsGet.js +5 -11
- package/src/handlers/standards/projectStandardsPut.js +19 -14
- package/src/handlers/standards/standardsParseUpload.js +4 -8
- package/src/handlers/standards/standardsRelevantPost.js +126 -29
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { executeQuery } = require('./index');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get standards that are consistently relevant for a project (>80% activation rate over 30 days)
|
|
5
|
+
*/
|
|
6
|
+
async function getPredictedStandards(projectId) {
|
|
7
|
+
const result = await executeQuery(`
|
|
8
|
+
WITH activation_stats AS (
|
|
9
|
+
SELECT
|
|
10
|
+
standard_id,
|
|
11
|
+
COUNT(*) as activation_count,
|
|
12
|
+
COUNT(DISTINCT DATE(activated_at)) as active_days
|
|
13
|
+
FROM rapport.standards_activation_log
|
|
14
|
+
WHERE project_id = $1
|
|
15
|
+
AND activated_at > NOW() - INTERVAL '30 days'
|
|
16
|
+
GROUP BY standard_id
|
|
17
|
+
),
|
|
18
|
+
total_sessions AS (
|
|
19
|
+
SELECT COUNT(DISTINCT DATE(activated_at)) as total_days
|
|
20
|
+
FROM rapport.standards_activation_log
|
|
21
|
+
WHERE project_id = $1
|
|
22
|
+
AND activated_at > NOW() - INTERVAL '30 days'
|
|
23
|
+
)
|
|
24
|
+
SELECT a.standard_id,
|
|
25
|
+
a.activation_count,
|
|
26
|
+
a.active_days,
|
|
27
|
+
t.total_days,
|
|
28
|
+
CASE WHEN t.total_days > 0
|
|
29
|
+
THEN ROUND(a.active_days::numeric / t.total_days * 100, 1)
|
|
30
|
+
ELSE 0 END as activation_rate
|
|
31
|
+
FROM activation_stats a, total_sessions t
|
|
32
|
+
WHERE t.total_days >= 5
|
|
33
|
+
AND a.active_days::numeric / GREATEST(t.total_days, 1) > 0.8
|
|
34
|
+
ORDER BY activation_rate DESC
|
|
35
|
+
`, [projectId]);
|
|
36
|
+
return result.rows;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Log which standards were activated for a project
|
|
41
|
+
*/
|
|
42
|
+
async function logStandardsActivation(projectId, standardIds) {
|
|
43
|
+
if (!standardIds || standardIds.length === 0) return;
|
|
44
|
+
const values = standardIds.map((id, i) => `($1, $${i + 2})`).join(', ');
|
|
45
|
+
await executeQuery(
|
|
46
|
+
`INSERT INTO rapport.standards_activation_log (project_id, standard_id) VALUES ${values}`,
|
|
47
|
+
[projectId, ...standardIds]
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { getPredictedStandards, logStandardsActivation };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Access Verification
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the "can this user access this project?" check.
|
|
5
|
+
* Grants access if the user is either:
|
|
6
|
+
* 1. A direct project collaborator (rapport.project_collaborators)
|
|
7
|
+
* 2. A member of the company that owns the project (rapport.user_entitlements)
|
|
8
|
+
*
|
|
9
|
+
* Returns the project row (project_id, project_name, company_id) on success,
|
|
10
|
+
* or null if the user has no access.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { executeQuery } = require('./dbOperations');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Verify a user has access to a project via collaborator role or company membership.
|
|
17
|
+
* @param {string} projectId - Project ID to check
|
|
18
|
+
* @param {string} email - User's email address
|
|
19
|
+
* @returns {Promise<{project_id: string, project_name: string, company_id: string, access_type: string}|null>}
|
|
20
|
+
* Project row with access_type ('collaborator' or 'company_member'), or null if no access.
|
|
21
|
+
*/
|
|
22
|
+
async function verifyProjectAccess(projectId, email) {
|
|
23
|
+
const result = await executeQuery(`
|
|
24
|
+
SELECT
|
|
25
|
+
p.project_id,
|
|
26
|
+
p.project_name,
|
|
27
|
+
p.company_id,
|
|
28
|
+
CASE
|
|
29
|
+
WHEN pc.email_address IS NOT NULL THEN 'collaborator'
|
|
30
|
+
WHEN ue.email_address IS NOT NULL THEN 'company_member'
|
|
31
|
+
END as access_type
|
|
32
|
+
FROM rapport.projects p
|
|
33
|
+
LEFT JOIN rapport.project_collaborators pc
|
|
34
|
+
ON p.project_id = pc.project_id
|
|
35
|
+
AND pc.email_address = $2
|
|
36
|
+
LEFT JOIN rapport.user_entitlements ue
|
|
37
|
+
ON p.company_id = ue.company_id
|
|
38
|
+
AND ue.email_address = $2
|
|
39
|
+
WHERE p.project_id = $1
|
|
40
|
+
AND (pc.email_address IS NOT NULL OR ue.email_address IS NOT NULL)
|
|
41
|
+
LIMIT 1
|
|
42
|
+
`, [projectId, email]);
|
|
43
|
+
|
|
44
|
+
return result.rows.length > 0 ? result.rows[0] : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Verify a user has a specific collaborator role on a project,
|
|
49
|
+
* OR is a company admin (which grants equivalent access).
|
|
50
|
+
* @param {string} projectId - Project ID to check
|
|
51
|
+
* @param {string} email - User's email address
|
|
52
|
+
* @param {string[]} allowedRoles - Collaborator roles that grant access (e.g. ['owner', 'admin'])
|
|
53
|
+
* @returns {Promise<{project_id: string, project_name: string, company_id: string, role: string|null, company_admin: boolean}|null>}
|
|
54
|
+
*/
|
|
55
|
+
async function verifyProjectRole(projectId, email, allowedRoles = ['owner', 'admin', 'collaborator']) {
|
|
56
|
+
const result = await executeQuery(`
|
|
57
|
+
SELECT
|
|
58
|
+
p.project_id,
|
|
59
|
+
p.project_name,
|
|
60
|
+
p.company_id,
|
|
61
|
+
pc.role,
|
|
62
|
+
COALESCE(ue.admin, false) as company_admin
|
|
63
|
+
FROM rapport.projects p
|
|
64
|
+
LEFT JOIN rapport.project_collaborators pc
|
|
65
|
+
ON p.project_id = pc.project_id
|
|
66
|
+
AND pc.email_address = $2
|
|
67
|
+
LEFT JOIN rapport.user_entitlements ue
|
|
68
|
+
ON p.company_id = ue.company_id
|
|
69
|
+
AND ue.email_address = $2
|
|
70
|
+
WHERE p.project_id = $1
|
|
71
|
+
AND (pc.email_address IS NOT NULL OR ue.email_address IS NOT NULL)
|
|
72
|
+
LIMIT 1
|
|
73
|
+
`, [projectId, email]);
|
|
74
|
+
|
|
75
|
+
if (result.rows.length === 0) return null;
|
|
76
|
+
|
|
77
|
+
const row = result.rows[0];
|
|
78
|
+
const hasRole = row.role && allowedRoles.includes(row.role);
|
|
79
|
+
const isCompanyAdmin = row.company_admin === true;
|
|
80
|
+
|
|
81
|
+
if (hasRole || isCompanyAdmin) {
|
|
82
|
+
return row;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { verifyProjectAccess, verifyProjectRole };
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implements MCP Streamable HTTP transport (protocol version 2025-03-26):
|
|
5
5
|
* - POST: JSON-RPC messages, responds with JSON or SSE stream
|
|
6
|
-
* - GET:
|
|
6
|
+
* - GET: SSE handshake or OAuth discovery
|
|
7
7
|
* - DELETE: Session close
|
|
8
8
|
* - OPTIONS: CORS preflight
|
|
9
9
|
*
|
|
10
|
-
* Auth: X-MindMeld-Token
|
|
10
|
+
* Auth: OAuth 2.1 (Cognito JWT) OR X-MindMeld-Token / Bearer API token
|
|
11
|
+
*
|
|
12
|
+
* OAuth Discovery (RFC 9728):
|
|
13
|
+
* GET /.well-known/oauth-protected-resource → resource metadata
|
|
14
|
+
* 401 responses include WWW-Authenticate with resource_metadata URL
|
|
11
15
|
*
|
|
12
16
|
* Uses awslambda.streamifyResponse() for Lambda Function URL streaming.
|
|
13
17
|
* The `awslambda` global is provided by the Lambda Node.js runtime.
|
|
@@ -16,6 +20,11 @@
|
|
|
16
20
|
const { validateApiToken, handleJsonRpc, CORS_HEADERS } = require('./mindmeldMcpCore');
|
|
17
21
|
const crypto = require('crypto');
|
|
18
22
|
|
|
23
|
+
// OAuth / Cognito configuration
|
|
24
|
+
const COGNITO_ISSUER = 'https://cognito-idp.us-east-2.amazonaws.com/us-east-2_638OhwuV1';
|
|
25
|
+
const COGNITO_AUTH_DOMAIN = 'https://mindmeld-auth.auth.us-east-2.amazoncognito.com';
|
|
26
|
+
const MCP_OAUTH_CLIENT_ID = '5bjpug8up86so7k7ndttobc0qi';
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* Write an SSE event to the response stream
|
|
21
30
|
*/
|
|
@@ -50,9 +59,98 @@ function createStream(responseStream, statusCode, headers) {
|
|
|
50
59
|
});
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Build the Function URL base from the event
|
|
64
|
+
*/
|
|
65
|
+
function getBaseUrl(event) {
|
|
66
|
+
const host = event.headers?.host || '';
|
|
67
|
+
return `https://${host}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Return OAuth Protected Resource Metadata (RFC 9728)
|
|
72
|
+
* Points to ourselves as the authorization server so we can serve
|
|
73
|
+
* metadata that includes code_challenge_methods_supported (PKCE).
|
|
74
|
+
* Actual OAuth endpoints still point to Cognito.
|
|
75
|
+
*/
|
|
76
|
+
function getResourceMetadata(baseUrl) {
|
|
77
|
+
return {
|
|
78
|
+
resource: baseUrl,
|
|
79
|
+
authorization_servers: [baseUrl],
|
|
80
|
+
scopes_supported: ['mcp/standards', 'openid', 'email', 'profile'],
|
|
81
|
+
bearer_methods_supported: ['header'],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return OAuth Authorization Server Metadata (RFC 8414)
|
|
87
|
+
* Wraps Cognito's endpoints with PKCE support declaration.
|
|
88
|
+
* Cognito supports PKCE but doesn't advertise it in OIDC metadata.
|
|
89
|
+
*/
|
|
90
|
+
function getAuthServerMetadata(baseUrl) {
|
|
91
|
+
return {
|
|
92
|
+
issuer: baseUrl,
|
|
93
|
+
authorization_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/authorize`,
|
|
94
|
+
token_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/token`,
|
|
95
|
+
revocation_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/revoke`,
|
|
96
|
+
userinfo_endpoint: `${COGNITO_AUTH_DOMAIN}/oauth2/userInfo`,
|
|
97
|
+
jwks_uri: `${COGNITO_ISSUER}/.well-known/jwks.json`,
|
|
98
|
+
scopes_supported: ['mcp/standards', 'openid', 'email', 'profile'],
|
|
99
|
+
response_types_supported: ['code'],
|
|
100
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
101
|
+
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
|
|
102
|
+
code_challenge_methods_supported: ['S256'],
|
|
103
|
+
subject_types_supported: ['public'],
|
|
104
|
+
id_token_signing_alg_values_supported: ['RS256'],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return 401 with WWW-Authenticate header per MCP OAuth spec
|
|
110
|
+
*/
|
|
111
|
+
function write401(stream, baseUrl) {
|
|
112
|
+
stream.write(JSON.stringify({
|
|
113
|
+
error: 'unauthorized',
|
|
114
|
+
message: 'Authentication required. Use OAuth or provide X-MindMeld-Token header.',
|
|
115
|
+
}));
|
|
116
|
+
stream.end();
|
|
117
|
+
}
|
|
118
|
+
|
|
53
119
|
// Lambda Function URL streaming handler
|
|
54
|
-
const streamHandler = async (event, responseStream,
|
|
120
|
+
const streamHandler = async (event, responseStream, context) => {
|
|
121
|
+
// CRITICAL: Don't wait for event loop to drain — the cached pg client
|
|
122
|
+
// keeps it alive forever, causing 900s timeouts on every request.
|
|
123
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
124
|
+
|
|
55
125
|
const { method, headers, body, isBase64Encoded } = parseRequest(event);
|
|
126
|
+
const path = parseRequest(event).path;
|
|
127
|
+
const baseUrl = getBaseUrl(event);
|
|
128
|
+
|
|
129
|
+
// Request logging for debugging
|
|
130
|
+
const hasAuth = !!(headers['authorization'] || headers['x-mindmeld-token']);
|
|
131
|
+
console.log(`[MCP-Stream] ${method} ${path} auth=${hasAuth} accept=${headers['accept'] || 'none'}`);
|
|
132
|
+
|
|
133
|
+
// === OAuth Discovery: Protected Resource Metadata (RFC 9728) ===
|
|
134
|
+
if (method === 'GET' && path === '/.well-known/oauth-protected-resource') {
|
|
135
|
+
const stream = createStream(responseStream, 200, {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'Cache-Control': 'public, max-age=3600',
|
|
138
|
+
});
|
|
139
|
+
stream.write(JSON.stringify(getResourceMetadata(baseUrl)));
|
|
140
|
+
stream.end();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// === OAuth Discovery: Authorization Server Metadata (RFC 8414) ===
|
|
145
|
+
if (method === 'GET' && path === '/.well-known/oauth-authorization-server') {
|
|
146
|
+
const stream = createStream(responseStream, 200, {
|
|
147
|
+
'Content-Type': 'application/json',
|
|
148
|
+
'Cache-Control': 'public, max-age=3600',
|
|
149
|
+
});
|
|
150
|
+
stream.write(JSON.stringify(getAuthServerMetadata(baseUrl)));
|
|
151
|
+
stream.end();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
56
154
|
|
|
57
155
|
// === OPTIONS: CORS preflight ===
|
|
58
156
|
if (method === 'OPTIONS') {
|
|
@@ -72,15 +170,18 @@ const streamHandler = async (event, responseStream, _context) => {
|
|
|
72
170
|
return;
|
|
73
171
|
}
|
|
74
172
|
|
|
75
|
-
//
|
|
173
|
+
// WWW-Authenticate header for 401 responses
|
|
174
|
+
const wwwAuth = `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`;
|
|
175
|
+
|
|
176
|
+
// === GET: SSE handshake ===
|
|
76
177
|
if (method === 'GET') {
|
|
77
178
|
const authResult = await validateApiToken(headers);
|
|
78
179
|
if (authResult.error) {
|
|
79
180
|
const stream = createStream(responseStream, 401, {
|
|
80
181
|
'Content-Type': 'application/json',
|
|
182
|
+
'WWW-Authenticate': wwwAuth,
|
|
81
183
|
});
|
|
82
|
-
stream
|
|
83
|
-
stream.end();
|
|
184
|
+
write401(stream, baseUrl);
|
|
84
185
|
return;
|
|
85
186
|
}
|
|
86
187
|
|
|
@@ -93,7 +194,7 @@ const streamHandler = async (event, responseStream, _context) => {
|
|
|
93
194
|
});
|
|
94
195
|
|
|
95
196
|
// Send endpoint event — legacy SSE clients expect this
|
|
96
|
-
stream.write(`event: endpoint\ndata: ${
|
|
197
|
+
stream.write(`event: endpoint\ndata: ${path}\n\n`);
|
|
97
198
|
|
|
98
199
|
// For Streamable HTTP clients probing via GET: close after endpoint event.
|
|
99
200
|
// The client will then POST to us for actual JSON-RPC messages.
|
|
@@ -114,17 +215,15 @@ const streamHandler = async (event, responseStream, _context) => {
|
|
|
114
215
|
// Auth
|
|
115
216
|
const authResult = await validateApiToken(headers);
|
|
116
217
|
if (authResult.error) {
|
|
117
|
-
|
|
218
|
+
console.log(`[MCP-Stream] POST auth failed: ${authResult.error} - ${authResult.message}`);
|
|
219
|
+
const stream = createStream(responseStream, 401, {
|
|
118
220
|
'Content-Type': 'application/json',
|
|
221
|
+
'WWW-Authenticate': wwwAuth,
|
|
119
222
|
});
|
|
120
|
-
stream
|
|
121
|
-
jsonrpc: '2.0',
|
|
122
|
-
error: { code: -32000, message: authResult.message },
|
|
123
|
-
id: null,
|
|
124
|
-
}));
|
|
125
|
-
stream.end();
|
|
223
|
+
write401(stream, baseUrl);
|
|
126
224
|
return;
|
|
127
225
|
}
|
|
226
|
+
console.log(`[MCP-Stream] POST auth OK: ${authResult.user.email}`);
|
|
128
227
|
|
|
129
228
|
// Parse body (Function URL may base64-encode it)
|
|
130
229
|
let parsedBody;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Auth: Cognito JWT required
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, verifyProjectAccess } = require('./helpers');
|
|
10
10
|
|
|
11
11
|
async function getDiscoveries({ queryStringParameters, requestContext }) {
|
|
12
12
|
try {
|
|
@@ -23,13 +23,9 @@ async function getDiscoveries({ queryStringParameters, requestContext }) {
|
|
|
23
23
|
return createErrorResponse(400, 'project_id is required');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// Verify user has access to project
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
WHERE project_id = $1 AND email_address = $2
|
|
30
|
-
`, [projectId, email]);
|
|
31
|
-
|
|
32
|
-
if (accessResult.rowCount === 0) {
|
|
26
|
+
// Verify user has access to project (collaborator or company member)
|
|
27
|
+
const projectAccess = await verifyProjectAccess(projectId, email);
|
|
28
|
+
if (!projectAccess) {
|
|
33
29
|
return createErrorResponse(403, 'Access denied to project');
|
|
34
30
|
}
|
|
35
31
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Auth: Cognito JWT required
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, verifyProjectAccess } = require('./helpers');
|
|
10
10
|
|
|
11
11
|
async function getProjectStandards({ queryStringParameters, pathParameters, requestContext }) {
|
|
12
12
|
try {
|
|
@@ -22,19 +22,13 @@ async function getProjectStandards({ queryStringParameters, pathParameters, requ
|
|
|
22
22
|
return createErrorResponse(400, 'projectId is required');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Verify user has access to project
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
FROM rapport.project_collaborators pc
|
|
29
|
-
JOIN rapport.projects p ON pc.project_id = p.project_id
|
|
30
|
-
WHERE pc.project_id = $1 AND pc.email_address = $2
|
|
31
|
-
`, [projectId, email]);
|
|
32
|
-
|
|
33
|
-
if (accessResult.rowCount === 0) {
|
|
25
|
+
// Verify user has access to project (collaborator or company member)
|
|
26
|
+
const projectAccess = await verifyProjectAccess(projectId, email);
|
|
27
|
+
if (!projectAccess) {
|
|
34
28
|
return createErrorResponse(403, 'Access denied to project');
|
|
35
29
|
}
|
|
36
30
|
|
|
37
|
-
const projectName =
|
|
31
|
+
const projectName = projectAccess.project_name;
|
|
38
32
|
|
|
39
33
|
// Get project standards preferences
|
|
40
34
|
const prefsResult = await executeQuery(`
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Auth: Cognito JWT required
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, verifyProjectRole } = require('./helpers');
|
|
11
11
|
|
|
12
12
|
async function updateProjectStandards({ body, pathParameters, requestContext }) {
|
|
13
13
|
try {
|
|
@@ -27,21 +27,14 @@ async function updateProjectStandards({ body, pathParameters, requestContext })
|
|
|
27
27
|
enabled_categories,
|
|
28
28
|
standard_overrides,
|
|
29
29
|
critical_overrides,
|
|
30
|
-
catalog_id = 'equilateral-v1'
|
|
30
|
+
catalog_id = 'equilateral-v1',
|
|
31
|
+
standard_id,
|
|
32
|
+
load_bearing
|
|
31
33
|
} = body || {};
|
|
32
34
|
|
|
33
|
-
// Verify user has admin access to project
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
WHERE project_id = $1 AND email_address = $2
|
|
37
|
-
`, [projectId, email]);
|
|
38
|
-
|
|
39
|
-
if (accessResult.rowCount === 0) {
|
|
40
|
-
return createErrorResponse(403, 'Access denied to project');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const role = accessResult.rows[0].role;
|
|
44
|
-
if (role !== 'owner' && role !== 'admin') {
|
|
35
|
+
// Verify user has admin access to project (project owner/admin or company admin)
|
|
36
|
+
const projectAccess = await verifyProjectRole(projectId, email, ['owner', 'admin']);
|
|
37
|
+
if (!projectAccess) {
|
|
45
38
|
return createErrorResponse(403, 'Admin access required to modify standards preferences');
|
|
46
39
|
}
|
|
47
40
|
|
|
@@ -70,6 +63,18 @@ async function updateProjectStandards({ body, pathParameters, requestContext })
|
|
|
70
63
|
}
|
|
71
64
|
}
|
|
72
65
|
|
|
66
|
+
// Update load_bearing flag on a specific standard if provided
|
|
67
|
+
if (standard_id && typeof load_bearing === 'boolean') {
|
|
68
|
+
await executeQuery(`
|
|
69
|
+
UPDATE rapport.standards_patterns
|
|
70
|
+
SET load_bearing = $1
|
|
71
|
+
WHERE pattern_id = $2
|
|
72
|
+
AND (company_id IS NULL OR company_id = (
|
|
73
|
+
SELECT company_id FROM rapport.user_entitlements WHERE email_address = $3 LIMIT 1
|
|
74
|
+
))
|
|
75
|
+
`, [load_bearing, standard_id, email]);
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
// Upsert project standards
|
|
74
79
|
const result = await executeQuery(`
|
|
75
80
|
INSERT INTO rapport.project_standards (
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* parsed YAML-compatible standards ready for storage or review.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
13
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, verifyProjectAccess } = require('./helpers');
|
|
14
14
|
|
|
15
15
|
const { parseAdr } = require('./core/parsers/adrParser');
|
|
16
16
|
const { parseEslint } = require('./core/parsers/eslintParser');
|
|
@@ -49,13 +49,9 @@ async function parseUploadStandards({ body, requestContext }) {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Verify user has access to the project
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
WHERE project_id = $1 AND email_address = $2
|
|
56
|
-
`, [project_id, email]);
|
|
57
|
-
|
|
58
|
-
if (accessResult.rowCount === 0) {
|
|
52
|
+
// Verify user has access to the project (collaborator or company member)
|
|
53
|
+
const projectAccess = await verifyProjectAccess(project_id, email);
|
|
54
|
+
if (!projectAccess) {
|
|
59
55
|
return createErrorResponse(403, 'Access denied to project');
|
|
60
56
|
}
|
|
61
57
|
|