crewly 1.20.265 → 1.20.266
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/config/skills/agent/core/slack-file/SKILL.md +53 -0
- package/config/skills/agent/core/slack-file/execute.sh +72 -0
- package/config/skills/agent/core/slack-file/execute.test.sh +34 -0
- package/dist/backend/backend/src/constants.d.ts +56 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +56 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/connector/connector.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/connector/connector.routes.js +6 -1
- package/dist/backend/backend/src/controllers/connector/connector.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts +69 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js +203 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.d.ts +16 -0
- package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.js +126 -5
- package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +37 -0
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +5 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +2 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +4 -3
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.d.ts +43 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.d.ts.map +1 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.js +98 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.js.map +1 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.d.ts +277 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.js +719 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.js.map +1 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.d.ts +29 -5
- package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.js +40 -12
- package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.js.map +1 -1
- package/dist/backend/backend/src/services/connector/remote-mcp-oauth.d.ts +196 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-oauth.d.ts.map +1 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-oauth.js +351 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-oauth.js.map +1 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.d.ts +3 -1
- package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.js +8 -0
- package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.js.map +1 -1
- package/dist/backend/backend/src/services/core/credential-files.d.ts.map +1 -1
- package/dist/backend/backend/src/services/core/credential-files.js +2 -0
- package/dist/backend/backend/src/services/core/credential-files.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-file-fetch.service.d.ts +119 -0
- package/dist/backend/backend/src/services/slack/slack-file-fetch.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-file-fetch.service.js +415 -0
- package/dist/backend/backend/src/services/slack/slack-file-fetch.service.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-file-ref.d.ts +69 -0
- package/dist/backend/backend/src/services/slack/slack-file-ref.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-file-ref.js +177 -0
- package/dist/backend/backend/src/services/slack/slack-file-ref.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js +16 -0
- package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +15 -0
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +84 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/types/slack.types.d.ts +11 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +56 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +56 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +2 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.d.ts +29 -5
- package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.js +40 -12
- package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.js.map +1 -1
- package/dist/cli/backend/src/services/core/credential-files.d.ts.map +1 -1
- package/dist/cli/backend/src/services/core/credential-files.js +2 -0
- package/dist/cli/backend/src/services/core/credential-files.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-file-fetch.service.d.ts +119 -0
- package/dist/cli/backend/src/services/slack/slack-file-fetch.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-file-fetch.service.js +415 -0
- package/dist/cli/backend/src/services/slack/slack-file-fetch.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-file-ref.d.ts +69 -0
- package/dist/cli/backend/src/services/slack/slack-file-ref.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-file-ref.js +177 -0
- package/dist/cli/backend/src/services/slack/slack-file-ref.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +15 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +84 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/types/slack.types.d.ts +11 -0
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/frontend/dist/assets/{index-83188f0b.js → index-7e5ebf78.js} +111 -111
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack file fetch — get a Slack file by link or id, even one your own bot
|
|
3
|
+
* cannot see.
|
|
4
|
+
*
|
|
5
|
+
* Slack serves a file only to members of a channel it was shared in. An
|
|
6
|
+
* agent asked to work on a colleague's file from another channel, often
|
|
7
|
+
* posted from another machine so the event never reached this one, had no
|
|
8
|
+
* way to open it. Rex asked Ella to "paste the text into the thread"
|
|
9
|
+
* instead (2026-10-08, `longform-en.md` in #pro-crewly-marketing).
|
|
10
|
+
*
|
|
11
|
+
* Order:
|
|
12
|
+
* 1. Locally, with the bot tokens this instance holds: the requesting
|
|
13
|
+
* agent's own bot, the uploader's bot, bots of agents in the channel,
|
|
14
|
+
* the workspace bot, then any other installed identity. The first one
|
|
15
|
+
* `files.info` answers for downloads the file.
|
|
16
|
+
* 2. Otherwise Crewly Cloud (`POST /api/cloud/slack/files/fetch`), which
|
|
17
|
+
* holds every bot of the account and tries them the same way.
|
|
18
|
+
*
|
|
19
|
+
* Files land in the same directory as inbound Slack attachments
|
|
20
|
+
* (`~/.crewly/tmp/slack-files/<id>-<name>`).
|
|
21
|
+
*
|
|
22
|
+
* @module services/slack/slack-file-fetch
|
|
23
|
+
*/
|
|
24
|
+
import { promises as fs } from 'fs';
|
|
25
|
+
import os from 'os';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
28
|
+
import { SLACK_FILE_DOWNLOAD_CONSTANTS, SLACK_FILE_FETCH_CONSTANTS } from '../../constants.js';
|
|
29
|
+
import { isSlackDownloadHost, isTextFile, parseSlackFileRef } from './slack-file-ref.js';
|
|
30
|
+
/** HTTP status per code. */
|
|
31
|
+
export const SLACK_FILE_GET_STATUS = {
|
|
32
|
+
validation: 400,
|
|
33
|
+
not_visible: 404,
|
|
34
|
+
no_file_in_message: 404,
|
|
35
|
+
file_deleted: 410,
|
|
36
|
+
too_large: 413,
|
|
37
|
+
foreign_workspace: 403,
|
|
38
|
+
cloud_unavailable: 503,
|
|
39
|
+
slack_error: 502,
|
|
40
|
+
};
|
|
41
|
+
/** A failure with a stable code. */
|
|
42
|
+
export class SlackFileGetError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.name = 'SlackFileGetError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The service — see module docs.
|
|
52
|
+
*/
|
|
53
|
+
export class SlackFileFetchService {
|
|
54
|
+
deps;
|
|
55
|
+
logger;
|
|
56
|
+
fetchImpl;
|
|
57
|
+
apiBase;
|
|
58
|
+
saveDir;
|
|
59
|
+
constructor(deps) {
|
|
60
|
+
this.deps = deps;
|
|
61
|
+
this.logger = LoggerService.getInstance().createComponentLogger('SlackFileFetch');
|
|
62
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
63
|
+
this.apiBase = deps.slackApiBase ?? 'https://slack.com/api';
|
|
64
|
+
this.saveDir = deps.saveDir ?? path.join(os.homedir(), '.crewly', SLACK_FILE_DOWNLOAD_CONSTANTS.TEMP_DIR);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fetch a file: locally first, then through Cloud.
|
|
68
|
+
*
|
|
69
|
+
* @param fileRef - File id, permalink, download URL or message link
|
|
70
|
+
* @param opts - `requesterAgent` (preferred bot, audit), `maxBytes` (default 25 MB)
|
|
71
|
+
* @returns Metadata and the saved path
|
|
72
|
+
* @throws {SlackFileGetError}
|
|
73
|
+
*/
|
|
74
|
+
async get(fileRef, opts = {}) {
|
|
75
|
+
const ref = parseSlackFileRef(fileRef);
|
|
76
|
+
if (!ref || (!ref.fileId && !ref.messageTs)) {
|
|
77
|
+
throw new SlackFileGetError('validation', 'Not a Slack file reference: pass a file id (F…), a file link, or a message link');
|
|
78
|
+
}
|
|
79
|
+
const maxBytes = Math.min(opts.maxBytes ?? SLACK_FILE_FETCH_CONSTANTS.MAX_BYTES, SLACK_FILE_FETCH_CONSTANTS.MAX_BYTES);
|
|
80
|
+
let localError = null;
|
|
81
|
+
try {
|
|
82
|
+
const local = await this.getLocal(ref, opts.requesterAgent, maxBytes);
|
|
83
|
+
if (local)
|
|
84
|
+
return local;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (!(err instanceof SlackFileGetError))
|
|
88
|
+
throw err;
|
|
89
|
+
// Final answers: the file is gone, too big, or not a file at all.
|
|
90
|
+
if (err.code === 'file_deleted' || err.code === 'too_large' || err.code === 'validation')
|
|
91
|
+
throw err;
|
|
92
|
+
localError = err;
|
|
93
|
+
}
|
|
94
|
+
const cloud = this.deps.cloud();
|
|
95
|
+
if (!cloud) {
|
|
96
|
+
throw localError ??
|
|
97
|
+
new SlackFileGetError('not_visible', 'No bot on this machine can see that file, and this machine is not signed in to Crewly Cloud (which could try the account\'s other bots).');
|
|
98
|
+
}
|
|
99
|
+
return this.getViaCloud(fileRef, cloud, opts.requesterAgent, maxBytes);
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Local
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
async getLocal(ref, requesterAgent, maxBytes) {
|
|
105
|
+
const candidates = (await this.deps.localCandidates(ref, requesterAgent)).slice(0, SLACK_FILE_FETCH_CONSTANTS.MAX_LOCAL_CANDIDATES);
|
|
106
|
+
if (candidates.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
let fileId = ref.fileId;
|
|
109
|
+
let otherFileIds;
|
|
110
|
+
if (!fileId) {
|
|
111
|
+
const found = await this.fileFromMessage(candidates, ref);
|
|
112
|
+
if (!found)
|
|
113
|
+
return null;
|
|
114
|
+
fileId = found.fileId;
|
|
115
|
+
otherFileIds = found.others.length > 0 ? found.others : undefined;
|
|
116
|
+
}
|
|
117
|
+
for (const c of candidates) {
|
|
118
|
+
const info = await this.slack('files.info', c.token, { file: fileId });
|
|
119
|
+
if (info.ok && info.data.file) {
|
|
120
|
+
const file = info.data.file;
|
|
121
|
+
const name = file.name || file.title || fileId;
|
|
122
|
+
const size = typeof file.size === 'number' ? file.size : 0;
|
|
123
|
+
if (size > maxBytes) {
|
|
124
|
+
throw new SlackFileGetError('too_large', `File is ${size} bytes; the limit is ${maxBytes}`);
|
|
125
|
+
}
|
|
126
|
+
const bytes = await this.download(file, c.token, maxBytes);
|
|
127
|
+
const meta = {
|
|
128
|
+
id: file.id || fileId,
|
|
129
|
+
name,
|
|
130
|
+
mimetype: file.mimetype || 'application/octet-stream',
|
|
131
|
+
size: bytes.length,
|
|
132
|
+
user: file.user ?? null,
|
|
133
|
+
channels: [...(file.channels ?? []), ...(file.groups ?? []), ...(file.ims ?? [])],
|
|
134
|
+
permalink: file.permalink ?? null,
|
|
135
|
+
isText: isTextFile(file.mimetype, name),
|
|
136
|
+
via: `local:${c.label}`,
|
|
137
|
+
...(otherFileIds ? { otherFileIds } : {}),
|
|
138
|
+
};
|
|
139
|
+
const localPath = await this.save(meta, bytes);
|
|
140
|
+
this.logger.info('Slack file fetched', { fileId: meta.id, via: meta.via, bytes: bytes.length });
|
|
141
|
+
return { meta, localPath };
|
|
142
|
+
}
|
|
143
|
+
if (!info.ok && info.error === 'file_deleted') {
|
|
144
|
+
throw new SlackFileGetError('file_deleted', 'That Slack file was deleted');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.logger.debug('No local bot can see the Slack file', { fileId, tried: candidates.length });
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
async fileFromMessage(candidates, ref) {
|
|
151
|
+
const ts = ref.messageTs;
|
|
152
|
+
for (const c of candidates) {
|
|
153
|
+
const threaded = !!ref.threadTs && ref.threadTs !== ts;
|
|
154
|
+
const res = await this.slack(threaded ? 'conversations.replies' : 'conversations.history', c.token, {
|
|
155
|
+
channel: ref.channelId,
|
|
156
|
+
...(threaded ? { ts: ref.threadTs } : {}),
|
|
157
|
+
latest: ts,
|
|
158
|
+
oldest: ts,
|
|
159
|
+
inclusive: 'true',
|
|
160
|
+
limit: threaded ? '5' : '1',
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok)
|
|
163
|
+
continue;
|
|
164
|
+
const message = (res.data.messages ?? []).find((m) => m.ts === ts);
|
|
165
|
+
if (!message)
|
|
166
|
+
continue;
|
|
167
|
+
const ids = (message.files ?? []).map((f) => f.id).filter((id) => !!id);
|
|
168
|
+
if (ids.length === 0)
|
|
169
|
+
throw new SlackFileGetError('no_file_in_message', 'That message has no file attached');
|
|
170
|
+
return { fileId: ids[0], others: ids.slice(1) };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
async download(file, token, maxBytes) {
|
|
175
|
+
let url = file.url_private_download || file.url_private;
|
|
176
|
+
if (!url)
|
|
177
|
+
throw new SlackFileGetError('slack_error', 'Slack returned no download URL for that file');
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), SLACK_FILE_FETCH_CONSTANTS.DOWNLOAD_TIMEOUT_MS);
|
|
180
|
+
try {
|
|
181
|
+
for (let hop = 0; hop <= SLACK_FILE_FETCH_CONSTANTS.MAX_REDIRECTS; hop++) {
|
|
182
|
+
const target = new URL(url);
|
|
183
|
+
if (target.protocol !== 'https:' || !isSlackDownloadHost(target.hostname)) {
|
|
184
|
+
throw new SlackFileGetError('slack_error', 'Slack redirected the download somewhere unexpected');
|
|
185
|
+
}
|
|
186
|
+
const sendAuth = target.hostname === 'slack.com' || target.hostname.endsWith('.slack.com');
|
|
187
|
+
const response = await this.fetchImpl(url, {
|
|
188
|
+
headers: sendAuth ? { Authorization: `Bearer ${token}` } : {},
|
|
189
|
+
redirect: 'manual',
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
});
|
|
192
|
+
if (response.status >= 300 && response.status < 400) {
|
|
193
|
+
const location = response.headers.get('location');
|
|
194
|
+
await response.body?.cancel().catch(() => undefined);
|
|
195
|
+
if (!location)
|
|
196
|
+
throw new SlackFileGetError('slack_error', 'Slack redirect without a location');
|
|
197
|
+
url = new URL(location, url).toString();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!response.ok)
|
|
201
|
+
throw new SlackFileGetError('slack_error', `Slack download failed with HTTP ${response.status}`);
|
|
202
|
+
const type = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
203
|
+
if (type.startsWith('text/html') && !(file.mimetype ?? '').toLowerCase().startsWith('text/html')) {
|
|
204
|
+
await response.body?.cancel().catch(() => undefined);
|
|
205
|
+
throw new SlackFileGetError('slack_error', 'Slack served a sign-in page instead of the file');
|
|
206
|
+
}
|
|
207
|
+
return await readCapped(response, maxBytes);
|
|
208
|
+
}
|
|
209
|
+
throw new SlackFileGetError('slack_error', 'Too many redirects downloading the file');
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (err instanceof SlackFileGetError)
|
|
213
|
+
throw err;
|
|
214
|
+
throw new SlackFileGetError('slack_error', `Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Cloud
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
async getViaCloud(fileRef, cloud, requesterAgent, maxBytes) {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const timer = setTimeout(() => controller.abort(), SLACK_FILE_FETCH_CONSTANTS.DOWNLOAD_TIMEOUT_MS);
|
|
226
|
+
let response;
|
|
227
|
+
try {
|
|
228
|
+
response = await this.fetchImpl(`${cloud.baseUrl.replace(/\/$/, '')}${SLACK_FILE_FETCH_CONSTANTS.CLOUD_FETCH_PATH}`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cloud.token}` },
|
|
231
|
+
body: JSON.stringify({ fileRef, ...(requesterAgent ? { requesterAgent } : {}) }),
|
|
232
|
+
signal: controller.signal,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
throw new SlackFileGetError('cloud_unavailable', `Crewly Cloud did not answer: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
const body = (await response.json().catch(() => null));
|
|
242
|
+
const code = body?.code;
|
|
243
|
+
const known = ['validation', 'not_visible', 'no_file_in_message', 'file_deleted', 'too_large', 'foreign_workspace'];
|
|
244
|
+
if (code && known.includes(code)) {
|
|
245
|
+
throw new SlackFileGetError(code, body?.error ?? code);
|
|
246
|
+
}
|
|
247
|
+
// 404 without a code = a Cloud that does not have this endpoint yet.
|
|
248
|
+
throw new SlackFileGetError('cloud_unavailable', `Crewly Cloud could not fetch the file (HTTP ${response.status}${body?.error ? `: ${body.error}` : ''})`);
|
|
249
|
+
}
|
|
250
|
+
const metaHeader = response.headers.get(SLACK_FILE_FETCH_CONSTANTS.CLOUD_META_HEADER);
|
|
251
|
+
let cloudMeta = {};
|
|
252
|
+
if (metaHeader) {
|
|
253
|
+
try {
|
|
254
|
+
cloudMeta = JSON.parse(Buffer.from(metaHeader, 'base64url').toString('utf8'));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
cloudMeta = {};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const bytes = await readCapped(response, maxBytes);
|
|
261
|
+
const id = typeof cloudMeta.id === 'string' && /^F[A-Z0-9]+$/.test(cloudMeta.id) ? cloudMeta.id : parseSlackFileRef(fileRef)?.fileId ?? 'file';
|
|
262
|
+
const name = typeof cloudMeta.name === 'string' && cloudMeta.name ? cloudMeta.name : id;
|
|
263
|
+
const mimetype = typeof cloudMeta.mimetype === 'string' && cloudMeta.mimetype ? cloudMeta.mimetype : 'application/octet-stream';
|
|
264
|
+
const meta = {
|
|
265
|
+
id,
|
|
266
|
+
name,
|
|
267
|
+
mimetype,
|
|
268
|
+
size: bytes.length,
|
|
269
|
+
user: typeof cloudMeta.user === 'string' ? cloudMeta.user : null,
|
|
270
|
+
channels: Array.isArray(cloudMeta.channels) ? cloudMeta.channels.filter((c) => typeof c === 'string') : [],
|
|
271
|
+
permalink: typeof cloudMeta.permalink === 'string' ? cloudMeta.permalink : null,
|
|
272
|
+
isText: isTextFile(mimetype, name),
|
|
273
|
+
via: `cloud:${typeof cloudMeta.via === 'string' ? cloudMeta.via : 'unknown'}`,
|
|
274
|
+
...(Array.isArray(cloudMeta.otherFileIds) ? { otherFileIds: cloudMeta.otherFileIds.filter((c) => typeof c === 'string') } : {}),
|
|
275
|
+
};
|
|
276
|
+
const localPath = await this.save(meta, bytes);
|
|
277
|
+
this.logger.info('Slack file fetched through Cloud', { fileId: meta.id, via: meta.via, bytes: bytes.length });
|
|
278
|
+
return { meta, localPath };
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Helpers
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
async save(meta, bytes) {
|
|
288
|
+
await fs.mkdir(this.saveDir, { recursive: true });
|
|
289
|
+
const safeName = path.basename(meta.name).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';
|
|
290
|
+
const safeId = meta.id.replace(/[^A-Za-z0-9]/g, '');
|
|
291
|
+
const localPath = path.join(this.saveDir, `${safeId}-${safeName}`);
|
|
292
|
+
await fs.writeFile(localPath, bytes);
|
|
293
|
+
return localPath;
|
|
294
|
+
}
|
|
295
|
+
async slack(method, token, params) {
|
|
296
|
+
const controller = new AbortController();
|
|
297
|
+
const timer = setTimeout(() => controller.abort(), SLACK_FILE_FETCH_CONSTANTS.API_TIMEOUT_MS);
|
|
298
|
+
try {
|
|
299
|
+
const res = await this.fetchImpl(`${this.apiBase}/${method}`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Bearer ${token}` },
|
|
302
|
+
body: new URLSearchParams(params),
|
|
303
|
+
signal: controller.signal,
|
|
304
|
+
});
|
|
305
|
+
const body = (await res.json().catch(() => null));
|
|
306
|
+
if (!body || body.ok !== true)
|
|
307
|
+
return { ok: false, error: body?.error ?? `http_${res.status}` };
|
|
308
|
+
return { ok: true, data: body };
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Read a response body, refusing more than `maxBytes`.
|
|
320
|
+
*
|
|
321
|
+
* @param response - Fetch response
|
|
322
|
+
* @param maxBytes - Cap
|
|
323
|
+
* @returns The bytes
|
|
324
|
+
* @throws {SlackFileGetError} `too_large`
|
|
325
|
+
*/
|
|
326
|
+
async function readCapped(response, maxBytes) {
|
|
327
|
+
const declared = Number(response.headers.get('content-length') ?? '');
|
|
328
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
329
|
+
await response.body?.cancel().catch(() => undefined);
|
|
330
|
+
throw new SlackFileGetError('too_large', `File is ${declared} bytes; the limit is ${maxBytes}`);
|
|
331
|
+
}
|
|
332
|
+
if (!response.body)
|
|
333
|
+
return Buffer.from(await response.arrayBuffer());
|
|
334
|
+
const reader = response.body.getReader();
|
|
335
|
+
const chunks = [];
|
|
336
|
+
let total = 0;
|
|
337
|
+
for (;;) {
|
|
338
|
+
const { done, value } = await reader.read();
|
|
339
|
+
if (done)
|
|
340
|
+
break;
|
|
341
|
+
total += value.byteLength;
|
|
342
|
+
if (total > maxBytes) {
|
|
343
|
+
await reader.cancel().catch(() => undefined);
|
|
344
|
+
throw new SlackFileGetError('too_large', `File is over the ${maxBytes}-byte limit`);
|
|
345
|
+
}
|
|
346
|
+
chunks.push(Buffer.from(value));
|
|
347
|
+
}
|
|
348
|
+
return Buffer.concat(chunks);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The first lines of a text file, for a one-glance preview.
|
|
352
|
+
*
|
|
353
|
+
* @param localPath - Saved file
|
|
354
|
+
* @returns Up to PREVIEW_LINES lines / PREVIEW_CHARS characters
|
|
355
|
+
*/
|
|
356
|
+
export async function previewTextFile(localPath) {
|
|
357
|
+
const text = await fs.readFile(localPath, 'utf8');
|
|
358
|
+
const lines = text.split('\n').slice(0, SLACK_FILE_FETCH_CONSTANTS.PREVIEW_LINES).join('\n');
|
|
359
|
+
return lines.length > SLACK_FILE_FETCH_CONSTANTS.PREVIEW_CHARS ? `${lines.slice(0, SLACK_FILE_FETCH_CONSTANTS.PREVIEW_CHARS)}…` : lines;
|
|
360
|
+
}
|
|
361
|
+
let instance = null;
|
|
362
|
+
/**
|
|
363
|
+
* The process-wide service, wired to this instance's Slack identities and
|
|
364
|
+
* Cloud sign-in. Built on first use.
|
|
365
|
+
*
|
|
366
|
+
* @returns The service
|
|
367
|
+
*/
|
|
368
|
+
export async function getSlackFileFetchService() {
|
|
369
|
+
if (instance)
|
|
370
|
+
return instance;
|
|
371
|
+
const { getSlackService } = await import('./slack.service.js');
|
|
372
|
+
const { getSlackAgentIdentityService } = await import('./slack-agent-identity.service.js');
|
|
373
|
+
const { getSlackTeamChannelService } = await import('./slack-team-channel.service.js');
|
|
374
|
+
const { CloudClientService } = await import('../cloud/cloud-client.service.js');
|
|
375
|
+
instance = new SlackFileFetchService({
|
|
376
|
+
localCandidates: async (ref, requesterAgent) => {
|
|
377
|
+
const identities = getSlackAgentIdentityService();
|
|
378
|
+
const out = [];
|
|
379
|
+
const push = (token, label) => {
|
|
380
|
+
if (token && !out.some((c) => c.token === token))
|
|
381
|
+
out.push({ token, label });
|
|
382
|
+
};
|
|
383
|
+
const tokenOf = (session) => session ? identities?.getInstalled(session)?.botToken ?? undefined : undefined;
|
|
384
|
+
if (requesterAgent)
|
|
385
|
+
push(tokenOf(requesterAgent), requesterAgent);
|
|
386
|
+
if (ref.uploaderUserId) {
|
|
387
|
+
const uploader = identities?.findByBotUserId(ref.uploaderUserId) ?? null;
|
|
388
|
+
if (uploader)
|
|
389
|
+
push(tokenOf(uploader), uploader);
|
|
390
|
+
}
|
|
391
|
+
if (ref.channelId) {
|
|
392
|
+
for (const session of getSlackTeamChannelService()?.rosterSessions(ref.channelId) ?? [])
|
|
393
|
+
push(tokenOf(session), session);
|
|
394
|
+
}
|
|
395
|
+
push(getSlackService().getBotToken(), 'workspace');
|
|
396
|
+
for (const r of (await identities?.list().catch(() => [])) ?? []) {
|
|
397
|
+
if (r.status === 'installed' && r.botToken)
|
|
398
|
+
push(r.botToken, r.agentSession);
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
},
|
|
402
|
+
cloud: () => {
|
|
403
|
+
const cloud = CloudClientService.getInstance();
|
|
404
|
+
const token = cloud.getToken();
|
|
405
|
+
const baseUrl = cloud.getCloudUrl();
|
|
406
|
+
return token && baseUrl ? { baseUrl, token } : null;
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
return instance;
|
|
410
|
+
}
|
|
411
|
+
/** Replace the process-wide service (tests). */
|
|
412
|
+
export function setSlackFileFetchService(service) {
|
|
413
|
+
instance = service;
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=slack-file-fetch.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-file-fetch.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/slack/slack-file-fetch.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack file references — turn whatever an agent was handed (a file id, a
|
|
3
|
+
* file permalink, a download URL, a message link) into the ids Slack's API
|
|
4
|
+
* needs.
|
|
5
|
+
*
|
|
6
|
+
* Kept in step with the Cloud copy in
|
|
7
|
+
* `crewly-services/auth/src/slack-file-ref.ts`; both sides parse
|
|
8
|
+
* the same links.
|
|
9
|
+
*
|
|
10
|
+
* @module services/slack/slack-file-ref
|
|
11
|
+
*/
|
|
12
|
+
/** What a reference names. */
|
|
13
|
+
export interface SlackFileRef {
|
|
14
|
+
/** Slack file id (`F…`), when the reference names one. */
|
|
15
|
+
fileId?: string;
|
|
16
|
+
/** Uploader's Slack user id, from `/files/<user>/<file>` permalinks. */
|
|
17
|
+
uploaderUserId?: string;
|
|
18
|
+
/** Slack team id embedded in `files-pri` / `files-tmb` / `slack-files.com` URLs. */
|
|
19
|
+
slackTeamId?: string;
|
|
20
|
+
/** Workspace host of a permalink (`acme.slack.com`). */
|
|
21
|
+
workspaceHost?: string;
|
|
22
|
+
/** Channel of a message link (`/archives/<channel>/p<ts>`). */
|
|
23
|
+
channelId?: string;
|
|
24
|
+
/** Message ts of a message link (`1696771234.567890`). */
|
|
25
|
+
messageTs?: string;
|
|
26
|
+
/** Thread parent ts of a message link (`?thread_ts=`). */
|
|
27
|
+
threadTs?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse a file reference.
|
|
31
|
+
*
|
|
32
|
+
* Accepted:
|
|
33
|
+
* - `F0123ABCD` (a bare file id)
|
|
34
|
+
* - `https://<ws>.slack.com/files/<user>/<FILE>/<name>` (file permalink)
|
|
35
|
+
* - `https://files.slack.com/files-pri/<TEAM>-<FILE>/<name>` (+ `/download/`)
|
|
36
|
+
* - `https://files.slack.com/files-tmb/<TEAM>-<FILE>-<hash>/<name>`
|
|
37
|
+
* - `https://slack-files.com/<TEAM>-<FILE>-<secret>` (public permalink)
|
|
38
|
+
* - `https://<ws>.slack.com/archives/<CHANNEL>/p<ts>[?thread_ts=…]` (a message carrying files)
|
|
39
|
+
* - any of the above wrapped Slack-style as `<url|label>`
|
|
40
|
+
*
|
|
41
|
+
* @param input - What the agent passed
|
|
42
|
+
* @returns The parsed reference, or null when it is not a Slack file reference
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseSlackFileRef(input: string): SlackFileRef | null;
|
|
45
|
+
/**
|
|
46
|
+
* Slack file and message links in a piece of text (deduplicated, in order).
|
|
47
|
+
*
|
|
48
|
+
* @param text - Message text
|
|
49
|
+
* @returns Links that {@link parseSlackFileRef} accepts
|
|
50
|
+
*/
|
|
51
|
+
export declare function findSlackFileLinks(text: string): string[];
|
|
52
|
+
/** Hosts a Slack file download may be served from (redirects included). */
|
|
53
|
+
export declare function isSlackDownloadHost(host: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Whether a file is text (so it can be handed back as a string).
|
|
56
|
+
*
|
|
57
|
+
* @param mimetype - Slack mimetype
|
|
58
|
+
* @param name - File name
|
|
59
|
+
* @returns True for text-like files
|
|
60
|
+
*/
|
|
61
|
+
export declare function isTextFile(mimetype: string | undefined, name: string | undefined): boolean;
|
|
62
|
+
/** Exposed for tests. */
|
|
63
|
+
export declare const SLACK_ID_PATTERNS: {
|
|
64
|
+
readonly FILE_ID: RegExp;
|
|
65
|
+
readonly USER_ID: RegExp;
|
|
66
|
+
readonly TEAM_ID: RegExp;
|
|
67
|
+
readonly CHANNEL_ID: RegExp;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=slack-file-ref.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-file-ref.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/slack/slack-file-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8BAA8B;AAC9B,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAuBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA8DpE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AAcD,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAoBD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAM1F;AAED,yBAAyB;AACzB,eAAO,MAAM,iBAAiB;;;;;CAAqD,CAAC"}
|