n8n
Version:
n8n Workflow Automation Tool
322 lines • 13.6 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SlackAppSetupService = void 0;
const node_crypto_1 = require("node:crypto");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const credentials_service_1 = require("../../../credentials/credentials.service");
const bad_request_error_1 = require("../../../errors/response-errors/bad-request.error");
const conflict_error_1 = require("../../../errors/response-errors/conflict.error");
const not_found_error_1 = require("../../../errors/response-errors/not-found.error");
const cache_service_1 = require("../../../services/cache/cache.service");
const url_service_1 = require("../../../services/url.service");
const agents_service_1 = require("../agents.service");
const agent_repository_1 = require("../repositories/agent.repository");
const chat_integration_service_1 = require("./chat-integration.service");
const SLACK_APP_SETUP_CACHE_PREFIX = 'agents:slack-app-setup:';
const SLACK_APP_SETUP_TTL_MS = 60 * 60 * 1000;
const DEFAULT_SLACK_APP_NAME = 'n8n Agent';
const SLACK_CREDENTIAL_TYPE = 'slackApi';
const REQUIRED_BOT_EVENTS = [
'app_mention',
'assistant_thread_started',
'assistant_thread_context_changed',
'message.channels',
'message.groups',
'message.im',
'message.mpim',
];
const REQUIRED_BOT_SCOPES = [
'app_mentions:read',
'assistant:write',
'channels:history',
'channels:join',
'channels:manage',
'channels:read',
'chat:write',
'chat:write.customize',
'files:read',
'files:write',
'groups:history',
'groups:read',
'im:history',
'im:read',
'im:write',
'mpim:history',
'mpim:read',
'mpim:write',
'search:read.public',
'users:read',
'users:read.email',
];
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function childRecord(record, key) {
const child = record[key];
return isRecord(child) ? child : undefined;
}
function stringProperty(record, key) {
const value = record?.[key];
return typeof value === 'string' ? value : undefined;
}
function hasSessionShape(value) {
const keys = [
'projectId',
'agentId',
'userId',
'appId',
'clientId',
'clientSecret',
'signingSecret',
'redirectUrl',
];
return isRecord(value) && keys.every((k) => typeof value[k] === 'string');
}
let SlackAppSetupService = class SlackAppSetupService {
constructor(cacheService, cipher, credentialsService, userRepository, agentRepository, agentsService, chatIntegrationService, urlService) {
this.cacheService = cacheService;
this.cipher = cipher;
this.credentialsService = credentialsService;
this.userRepository = userRepository;
this.agentRepository = agentRepository;
this.agentsService = agentsService;
this.chatIntegrationService = chatIntegrationService;
this.urlService = urlService;
}
async createApp(options) {
const agent = await this.getAgent(options.agentId, options.projectId, true);
const appConfigurationToken = options.appConfigurationToken.trim();
if (!appConfigurationToken) {
throw new bad_request_error_1.BadRequestError('Slack app configuration token is required');
}
const redirectUrl = this.callbackUrl(options.projectId, options.agentId);
const manifest = this.buildManifest(agent.name, options.projectId, options.agentId, {
redirectUrl,
});
const response = await this.callSlackApi('apps.manifest.create', {
token: appConfigurationToken,
manifest: JSON.stringify(manifest),
});
if (response.ok !== true) {
throw this.slackError('create the Slack app', response);
}
const credentials = childRecord(response, 'credentials');
const appId = stringProperty(response, 'app_id');
const clientId = stringProperty(credentials, 'client_id');
const clientSecret = stringProperty(credentials, 'client_secret');
const signingSecret = stringProperty(credentials, 'signing_secret');
const oauthAuthorizeUrl = stringProperty(response, 'oauth_authorize_url');
if (!appId || !clientId || !clientSecret || !signingSecret || !oauthAuthorizeUrl) {
throw new bad_request_error_1.BadRequestError('Slack returned an incomplete app setup response');
}
const state = (0, node_crypto_1.randomBytes)(32).toString('hex');
const setupSession = {
projectId: options.projectId,
agentId: options.agentId,
userId: options.user.id,
appId,
clientId,
clientSecret,
signingSecret,
redirectUrl,
};
await this.cacheService.set(this.cacheKey(state), await this.cipher.encryptV2(JSON.stringify(setupSession)), SLACK_APP_SETUP_TTL_MS);
return {
appId,
installUrl: this.installUrl(oauthAuthorizeUrl, state, redirectUrl),
};
}
async getManualManifest(options) {
const agent = await this.getAgent(options.agentId, options.projectId);
return {
manifest: this.buildManifest(agent.name, options.projectId, options.agentId),
};
}
async completeInstall(options) {
const session = await this.consumeSession(options.state);
if (session.projectId !== options.projectId || session.agentId !== options.agentId) {
throw new bad_request_error_1.BadRequestError('Slack app setup state does not match this agent');
}
const agent = await this.getAgent(session.agentId, session.projectId, true);
const user = await this.userRepository.findOne({
where: { id: session.userId },
relations: ['role'],
});
if (!user) {
throw new not_found_error_1.NotFoundError(`User "${session.userId}" not found`);
}
const tokenResponse = await this.callSlackApi('oauth.v2.access', {
code: options.code,
redirect_uri: session.redirectUrl,
}, {
Authorization: `Basic ${Buffer.from(`${session.clientId}:${session.clientSecret}`).toString('base64')}`,
});
if (tokenResponse.ok !== true) {
throw this.slackError('finish Slack app installation', tokenResponse);
}
const accessToken = stringProperty(tokenResponse, 'access_token');
if (!accessToken?.startsWith('xoxb-')) {
throw new bad_request_error_1.BadRequestError('Slack did not return a Bot User OAuth Token');
}
const credential = await this.credentialsService.createUnmanagedCredential({
name: this.credentialName(agent.name),
type: SLACK_CREDENTIAL_TYPE,
data: {
accessToken,
signatureSecret: session.signingSecret,
},
projectId: session.projectId,
}, user);
const integration = {
type: 'slack',
credentialId: credential.id,
};
await this.chatIntegrationService.connect(session.agentId, integration, session.userId, session.projectId);
await this.agentsService.saveCredentialIntegration(agent, integration);
}
async getAgent(agentId, projectId, requirePublished = false) {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent)
throw new not_found_error_1.NotFoundError(`Agent "${agentId}" not found`);
if (requirePublished && !agent.activeVersionId) {
throw new conflict_error_1.ConflictError(`Agent "${agentId}" must be published before connecting an integration`);
}
return agent;
}
buildManifest(agentName, projectId, agentId, options = {}) {
const slackAppName = this.sanitiseSlackAppName(agentName);
const webhookUrl = this.webhookUrl(projectId, agentId);
return {
display_information: {
name: slackAppName,
},
features: {
app_home: {
home_tab_enabled: true,
messages_tab_enabled: true,
messages_tab_read_only_enabled: false,
},
bot_user: {
display_name: slackAppName,
always_online: true,
},
},
oauth_config: {
...(options.redirectUrl ? { redirect_urls: [options.redirectUrl] } : {}),
scopes: {
bot: [...REQUIRED_BOT_SCOPES],
},
},
settings: {
event_subscriptions: {
request_url: webhookUrl,
bot_events: [...REQUIRED_BOT_EVENTS],
},
interactivity: {
is_enabled: true,
request_url: webhookUrl,
},
org_deploy_enabled: false,
socket_mode_enabled: false,
token_rotation_enabled: false,
},
};
}
webhookUrl(projectId, agentId) {
return `${this.urlService.getWebhookBaseUrl()}rest/projects/${projectId}/agents/v2/${agentId}/webhooks/slack`;
}
callbackUrl(projectId, agentId) {
return `${this.urlService.getWebhookBaseUrl()}rest/projects/${projectId}/agents/v2/${agentId}/integrations/slack/oauth/callback`;
}
installUrl(oauthAuthorizeUrl, state, redirectUrl) {
try {
const url = new URL(oauthAuthorizeUrl);
url.searchParams.set('state', state);
url.searchParams.set('redirect_uri', redirectUrl);
return url.toString();
}
catch {
throw new bad_request_error_1.BadRequestError('Slack returned an invalid installation URL');
}
}
async consumeSession(state) {
const key = this.cacheKey(state);
const cached = await this.cacheService.get(key);
await this.cacheService.delete(key);
if (typeof cached !== 'string') {
throw new bad_request_error_1.BadRequestError('Slack app setup state has expired or is invalid');
}
try {
const decrypted = await this.cipher.decryptV2(cached);
const session = (0, n8n_workflow_1.jsonParse)(decrypted, { fallbackValue: null });
if (hasSessionShape(session)) {
return session;
}
}
catch { }
throw new bad_request_error_1.BadRequestError('Slack app setup state has expired or is invalid');
}
cacheKey(state) {
return `${SLACK_APP_SETUP_CACHE_PREFIX}${state}`;
}
credentialName(agentName) {
return `Slack - ${agentName || DEFAULT_SLACK_APP_NAME}`.slice(0, 128);
}
sanitiseSlackAppName(raw) {
const cleaned = raw
.replace(/[^a-zA-Z0-9 ._-]/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 35);
return cleaned.length > 0 ? cleaned : DEFAULT_SLACK_APP_NAME;
}
async callSlackApi(method, params, headers = {}) {
try {
const response = await fetch(`https://slack.com/api/${method}`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(params).toString(),
});
const data = await response.json();
if (!isRecord(data)) {
return { ok: false, error: 'invalid_response' };
}
return data;
}
catch {
return { ok: false, error: 'slack_request_failed' };
}
}
slackError(action, response) {
const error = stringProperty(response, 'error') ?? 'unknown_error';
return new bad_request_error_1.BadRequestError(`Slack could not ${action}: ${error}`);
}
};
exports.SlackAppSetupService = SlackAppSetupService;
exports.SlackAppSetupService = SlackAppSetupService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [cache_service_1.CacheService,
n8n_core_1.Cipher,
credentials_service_1.CredentialsService,
db_1.UserRepository,
agent_repository_1.AgentRepository,
agents_service_1.AgentsService,
chat_integration_service_1.ChatIntegrationService,
url_service_1.UrlService])
], SlackAppSetupService);
//# sourceMappingURL=slack-app-setup.service.js.map