UNPKG

@jupiterone/jupiterone-mcp

Version:

Model Context Protocol server for JupiterOne account rules and rule details

123 lines 6.24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.accessDeniedStatus = accessDeniedStatus; exports.accessDeniedMessage = accessDeniedMessage; exports.accessDeniedResult = accessDeniedResult; exports.toErrorResponse = toErrorResponse; exports.validateQueries = validateQueries; exports.createValidationErrorResponse = createValidationErrorResponse; exports.createQueryErrorResponse = createQueryErrorResponse; const sanitize_error_js_1 = require("../../client/sanitize-error.js"); const ACCESS_DENIED_STATUS = new Set([401, 403]); // GraphQL `extensions.code` values that mean authorization failed, mapped to the HTTP status they // correspond to. The upstream frequently returns these inside an HTTP 200 (see JupiterOneApiError.code), // so the transport status alone misses them. const ACCESS_DENIED_CODE_STATUS = { FORBIDDEN: 403, UNAUTHENTICATED: 401, UNAUTHORIZED: 403, }; // Conservative last-resort match on the upstream message for authz failures that carry neither a // 401/403 status nor an extensions.code. Kept narrow so ordinary query errors are not misclassified. const ACCESS_DENIED_MESSAGE = /not authorized|access denied|forbidden|unauthenticated/i; /** * The HTTP status of an upstream authentication/authorization rejection, or `null` for any other * error. An account can stay in `list-accounts` (membership) yet fail live authorization — revoked, * disabled, or gated behind SSO — so these need their own message, not a raw envelope. Rejections * arrive three ways: a real 401/403, a GraphQL `extensions.code` (FORBIDDEN/UNAUTHENTICATED) inside * an HTTP 200, or, failing both, an authz-shaped message. Code/message hits map to the equivalent * status so callers get a consistent access-denied frame. */ function accessDeniedStatus(error) { if (!(error instanceof sanitize_error_js_1.JupiterOneApiError)) return null; if (error.statusCode !== undefined && ACCESS_DENIED_STATUS.has(error.statusCode)) { return error.statusCode; } if (error.code && error.code in ACCESS_DENIED_CODE_STATUS) { return ACCESS_DENIED_CODE_STATUS[error.code]; } if (ACCESS_DENIED_MESSAGE.test(error.message)) { return 403; } return null; } /** * Agent-facing message for an access-denied upstream response. The guidance is tenancy-aware: * `list-accounts` and "try another accountId" only make sense when the host supplies an account * resolver (multi-tenant). With a fixed account (single-tenant subdomain / env account) there is * no discovery tool and no account to switch to, so steer the caller to re-authenticate instead. */ function accessDeniedMessage(statusCode, hasAccountDiscovery) { const guidance = hasAccountDiscovery ? 'Use `list-accounts` to review your account memberships and try another accountId — note ' + 'that a listed account is not a guarantee of live access.' : 'Verify your access to this account and re-authenticate if needed.'; return (`Not authorized for this account (HTTP ${statusCode}). Your access may have been revoked, ` + `or the account may require a different sign-in method such as SSO. ${guidance}`); } function accessDeniedResult(statusCode, hasAccountDiscovery) { return { content: [{ type: 'text', text: accessDeniedMessage(statusCode, hasAccountDiscovery) }], isError: true, }; } /** * Generic error envelope for a thrown handler. Upstream GraphQL errors are already sanitized at * the shared client (S9), so `error.message` is safe to surface here. Access-denied (401/403) * errors are framed by the registration chokepoint before this is reached. */ function toErrorResponse(error, context) { const message = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: `${context}: ${message}` }], isError: true, }; } /** Validate each J1QL query in a rule/widget definition before the create/update call. */ async function validateQueries(queries, validator, options) { if (!queries || !Array.isArray(queries)) return []; const validationResults = []; for (const queryObj of queries) { if (queryObj.query) { const validation = await validator.validateQuery(queryObj.query, options); if (!validation.isValid) { validationResults.push({ queryName: queryObj.name || 'Unnamed query', error: validation.error || 'Query validation failed', suggestion: validation.suggestion || 'Please check the query syntax and try again', }); } } } return validationResults; } /** Tool result returned (as a non-throwing outcome) when pre-create query validation fails. */ function createValidationErrorResponse(validationResults) { return { content: [ { type: 'text', text: `Query validation failed. Please fix the following issues:\n\n${validationResults .map((r) => `Query: ${r.queryName}\nError: ${r.error}\nSuggestion: ${r.suggestion}`) .join('\n\n')}\n\nUse the execute-j1ql-query tool to test and refine your queries first.`, }, ], isError: true, }; } /** Validator-aware error for execute-j1ql-query: maps execution errors to actionable suggestions. */ function createQueryErrorResponse(error, query, validator) { const errorResult = validator.handleQueryError(error, query); return { content: [ { type: 'text', text: `Error executing J1QL query:\n\n${errorResult.error}\n\nSuggestion: ${errorResult.suggestion}\n\nDebugging tips:\n1. Modify existing queries that are already working as expected.\n2. Use discovery queries to understand available data\n3. Verify entity classes exist (use proper capitalization)\n4. Check property names match exactly\n5. Use single quotes for strings, not double quotes\n6. Place aliases after WITH statements\n7. Add LIMIT clause to prevent timeouts`, }, ], isError: true, }; } //# sourceMappingURL=errors.js.map