Skip to content

Commit 2094c54

Browse files
committed
Add Slack modern file upload parity
- Add stateful Slack external upload sessions, file records, reads, lists, and deletes. - Share uploaded files as Slack file_share messages with file events and inspector visibility. - Cover SDK uploadV2, binary upload, routes, docs, and coverage matrix updates.
1 parent 4f47d71 commit 2094c54

16 files changed

Lines changed: 1049 additions & 20 deletions

File tree

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,13 @@ slack:
265265
name: My Slack App
266266
redirect_uris:
267267
- http://localhost:3000/api/auth/callback/slack
268-
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write]
268+
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write, files:read, files:write]
269269
user_scopes: [users:read, users.profile:read]
270270
bot_name: my-bot
271271
tokens:
272272
- token: xoxb-local-test
273273
user: developer
274-
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write]
274+
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write, files:read, files:write]
275275
strict_scopes: false
276276

277277
apple:
@@ -612,7 +612,7 @@ OAuth 2.0, OpenID Connect, and mutable Google Workspace-style surfaces for local
612612

613613
## Slack API
614614

615-
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
615+
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
616616

617617
### Auth & Chat
618618
- `POST /api/auth.test` - test authentication
@@ -651,6 +651,14 @@ Fully stateful Slack Web API emulation with channels, messages, threads, reactio
651651
- `POST /api/users.setPresence` - set the authed user to away or automatic presence
652652
- `POST /api/reactions.add` / `reactions.remove` / `reactions.get` - manage reactions
653653

654+
### Files
655+
- `POST /api/files.getUploadURLExternal` - create a local external upload session
656+
- `POST /upload/v1/:fileId` - receive raw uploaded file bytes
657+
- `POST /api/files.completeUploadExternal` - complete uploads and optionally share file messages
658+
- `GET /api/files.info` / `POST /api/files.info` - get file metadata
659+
- `GET /api/files.list` / `POST /api/files.list` - list completed files
660+
- `POST /api/files.delete` - delete a completed file
661+
654662
### Team, Bots & Webhooks
655663
- `POST /api/team.info` - workspace info
656664
- `POST /api/bots.info` - bot info
@@ -660,7 +668,7 @@ Fully stateful Slack Web API emulation with channels, messages, threads, reactio
660668
- `GET /oauth/v2/authorize` - authorization (shows user picker)
661669
- `POST /api/oauth.v2.access` - token exchange
662670

663-
Slack scope checks are relaxed by default so local tests can use simple bearer tokens. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user and presence checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, and `users:write`.
671+
Slack scope checks are relaxed by default so local tests can use simple bearer tokens. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user, presence, and file checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, and `files:write`.
664672

665673
## Apple Sign In
666674

apps/web/app/docs/slack/page.mdx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Slack API
22

3-
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records. State changes dispatch `event_callback` payloads to configured webhook URLs.
3+
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records. State changes dispatch `event_callback` payloads to configured webhook URLs.
44

55
## Auth
66

77
- `POST /api/auth.test` - test authentication
88

9-
Scope checks are relaxed by default for local development. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user and presence checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, and `users:write`.
9+
Scope checks are relaxed by default for local development. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user, presence, and file checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, and `files:write`.
1010

1111
## Chat
1212

@@ -57,6 +57,15 @@ Scope checks are relaxed by default for local development. Set `slack.strict_sco
5757
- `POST /api/reactions.remove` - remove reaction
5858
- `POST /api/reactions.get` - get reactions for a message
5959

60+
## Files
61+
62+
- `POST /api/files.getUploadURLExternal` - create a local external upload session
63+
- `POST /upload/v1/:fileId` - receive raw uploaded file bytes
64+
- `POST /api/files.completeUploadExternal` - complete uploads and optionally share file messages
65+
- `GET /api/files.info` / `POST /api/files.info` - get file metadata
66+
- `GET /api/files.list` / `POST /api/files.list` - list completed files
67+
- `POST /api/files.delete` - delete a completed file
68+
6069
## Team
6170

6271
- `POST /api/team.info` - get workspace info
@@ -94,3 +103,5 @@ When messages are posted, updated, deleted, or reactions change, the emulator di
94103
- `im_created`, `im_open`, `im_close`, `im_marked`, and group open/close/marked events for DM and MPIM writes
95104
- `user_change` on profile writes
96105
- `presence_change` on presence writes
106+
- `file_created`, `file_shared`, and `file_deleted` on file writes
107+
- `message` with `subtype: file_share` on shared file uploads

packages/@emulators/slack/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @emulators/slack
22

3-
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
3+
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
44

55
Part of [emulate](https://github.com/vercel-labs/emulate) — local drop-in replacement services for CI and no-network sandboxes.
66

@@ -49,6 +49,14 @@ npm install @emulators/slack
4949
- `POST /api/users.setPresence` — set the authed user to away or automatic presence
5050
- `POST /api/reactions.add` / `reactions.remove` / `reactions.get` — manage reactions
5151

52+
### Files
53+
- `POST /api/files.getUploadURLExternal` — create a local external upload session
54+
- `POST /upload/v1/:fileId` — receive raw uploaded file bytes
55+
- `POST /api/files.completeUploadExternal` — complete uploads and optionally share file messages
56+
- `GET /api/files.info` / `POST /api/files.info` — get file metadata
57+
- `GET /api/files.list` / `POST /api/files.list` — list completed files
58+
- `POST /api/files.delete` — delete a completed file
59+
5260
### Team, Bots & Webhooks
5361
- `POST /api/team.info` — workspace info
5462
- `POST /api/bots.info` — bot info
@@ -60,7 +68,7 @@ npm install @emulators/slack
6068

6169
## Auth
6270

63-
All Web API endpoints require `Authorization: Bearer <token>`. Seeded OAuth apps create local installation state, and the OAuth v2 flow with user picker UI returns Slack-style bot tokens. Scope checks are relaxed by default for local development. Set `strict_scopes: true` in Slack seed config to return Slack-style `missing_scope` errors when a token lacks the required method scope. Supported user and presence checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, and `users:write`.
71+
All Web API endpoints require `Authorization: Bearer <token>`. Seeded OAuth apps create local installation state, and the OAuth v2 flow with user picker UI returns Slack-style bot tokens. Scope checks are relaxed by default for local development. Set `strict_scopes: true` in Slack seed config to return Slack-style `missing_scope` errors when a token lacks the required method scope. Supported user, presence, and file checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, and `files:write`.
6472

6573
## Seed Configuration
6674

@@ -98,6 +106,8 @@ slack:
98106
- users.profile:read
99107
- users.profile:write
100108
- users:write
109+
- files:read
110+
- files:write
101111
user_scopes:
102112
- users:read
103113
- users.profile:read
@@ -111,6 +121,8 @@ slack:
111121
- users.profile:read
112122
- users.profile:write
113123
- users:write
124+
- files:read
125+
- files:write
114126
strict_scopes: false
115127
```
116128

packages/@emulators/slack/src/__tests__/coverage.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ describe("Slack coverage matrix", () => {
4040

4141
it("keeps planned gaps explicit", () => {
4242
const planned = slackCoverageMatrix.filter((entry) => entry.status === "not_started");
43-
expect(planned.map((entry) => entry.method)).toEqual(
44-
expect.arrayContaining(["files.getUploadURLExternal", "views.publish"]),
45-
);
43+
expect(planned.map((entry) => entry.method)).toEqual(expect.arrayContaining(["views.publish"]));
4644
for (const entry of planned) {
4745
expect(entry.notes).toMatch(/Planned|future/i);
4846
}

packages/@emulators/slack/src/__tests__/slack-coverage.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,9 +384,57 @@ export const slackCoverageMatrix: SlackCoverageEntry[] = [
384384
family: "files",
385385
method: "files.getUploadURLExternal",
386386
route: "POST /api/files.getUploadURLExternal",
387-
status: "not_started",
388-
testedBy: [],
389-
notes: "Planned for SLACK-8.",
387+
status: "partial",
388+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
389+
notes: "Creates a local upload session and returns an emulator upload URL plus file id.",
390+
},
391+
{
392+
family: "files",
393+
method: "files.completeUploadExternal",
394+
route: "POST /api/files.completeUploadExternal",
395+
status: "partial",
396+
testedBy: ["slack.test.ts", "slack-sdk.test.ts", "slack-events.test.ts"],
397+
notes: "Completes uploaded bytes into file records, optional file_share messages, and file events.",
398+
},
399+
{
400+
family: "files",
401+
method: "files.info",
402+
route: ["GET /api/files.info", "POST /api/files.info"],
403+
status: "partial",
404+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
405+
notes: "Returns completed local file records.",
406+
},
407+
{
408+
family: "files",
409+
method: "files.list",
410+
route: ["GET /api/files.list", "POST /api/files.list"],
411+
status: "partial",
412+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
413+
notes: "Lists completed local files with simple channel, user, type, time, and paging filters.",
414+
},
415+
{
416+
family: "files",
417+
method: "files.delete",
418+
route: "POST /api/files.delete",
419+
status: "partial",
420+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
421+
notes: "Marks local files deleted and dispatches file_deleted events.",
422+
},
423+
{
424+
family: "files",
425+
method: "upload_url",
426+
route: "POST /upload/v1/:fileId",
427+
status: "partial",
428+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
429+
notes: "Receives raw upload bytes for files.getUploadURLExternal sessions.",
430+
},
431+
{
432+
family: "files",
433+
method: "files.private_download",
434+
route: "GET /files-pri/:fileId/:filename",
435+
status: "partial",
436+
testedBy: ["slack.test.ts"],
437+
notes: "Serves bytes for emulator url_private and url_private_download file links.",
390438
},
391439
{
392440
family: "views",

packages/@emulators/slack/src/__tests__/slack-events.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,58 @@ describe("Slack plugin - event dispatch baseline", () => {
251251
});
252252
});
253253

254+
it("dispatches file upload and file share events", async () => {
255+
const { app, store, webhooks } = createSlackTestApp();
256+
const capture = captureFetchRequests();
257+
registerSlackEventSubscription(webhooks, ["file_created", "file_shared", "message"]);
258+
259+
const channel = getSlackStore(store).channels.findOneBy("name", "general")!.channel_id;
260+
const urlRes = await app.request(`${base}/api/files.getUploadURLExternal`, {
261+
method: "POST",
262+
headers: authHeaders(),
263+
body: JSON.stringify({ filename: "events.txt", length: 12 }),
264+
});
265+
const upload = (await urlRes.json()) as any;
266+
await app.request(upload.upload_url, { method: "POST", body: "event file" });
267+
268+
await app.request(`${base}/api/files.completeUploadExternal`, {
269+
method: "POST",
270+
headers: authHeaders(),
271+
body: JSON.stringify({
272+
files: [{ id: upload.file_id, title: "Event File" }],
273+
channel_id: channel,
274+
initial_comment: "File event",
275+
}),
276+
});
277+
278+
expect(capture.requests).toHaveLength(3);
279+
expect(capture.jsonBodies()).toEqual([
280+
expect.objectContaining({
281+
event: expect.objectContaining({
282+
type: "file_created",
283+
file_id: upload.file_id,
284+
file: expect.objectContaining({ id: upload.file_id, title: "Event File" }),
285+
}),
286+
}),
287+
expect.objectContaining({
288+
event: expect.objectContaining({
289+
type: "file_shared",
290+
file_id: upload.file_id,
291+
channel_id: channel,
292+
}),
293+
}),
294+
expect.objectContaining({
295+
event: expect.objectContaining({
296+
type: "message",
297+
subtype: "file_share",
298+
channel,
299+
text: "File event",
300+
files: [expect.objectContaining({ id: upload.file_id })],
301+
}),
302+
}),
303+
]);
304+
});
305+
254306
it("dispatches message_changed events for chat.update", async () => {
255307
const { app, store, webhooks } = createSlackTestApp();
256308
const capture = captureFetchRequests();

packages/@emulators/slack/src/__tests__/slack-sdk.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,33 @@ describe("Slack plugin - real @slack/web-api WebClient baseline", () => {
386386
expect(bot.ok).toBe(true);
387387
expect(bot.bot?.name).toBe("test-bot");
388388
});
389+
390+
it("uploads files through the Slack SDK uploadV2 helper", async () => {
391+
expect(emulator).toBeDefined();
392+
const channel = getSlackStore(emulator!.store).channels.findOneBy("name", "general")!.channel_id;
393+
394+
const uploaded = (await client.files.uploadV2({
395+
channel_id: channel,
396+
content: "SDK upload body",
397+
filename: "sdk-upload.txt",
398+
title: "SDK Upload",
399+
initial_comment: "SDK file upload",
400+
})) as any;
401+
expect(uploaded.ok).toBe(true);
402+
const completed = (uploaded.files as any[])[0];
403+
const file = completed.files[0];
404+
expect(file.title).toBe("SDK Upload");
405+
expect(file.channels).toContain(channel);
406+
407+
const info = await client.files.info({ file: file.id });
408+
expect(info.ok).toBe(true);
409+
expect(info.file?.title).toBe("SDK Upload");
410+
411+
const list = await client.files.list({ channel });
412+
expect(list.ok).toBe(true);
413+
expect(list.files?.map((item) => item.id)).toContain(file.id);
414+
415+
const deleted = await client.files.delete({ file: file.id! });
416+
expect(deleted.ok).toBe(true);
417+
});
389418
});

0 commit comments

Comments
 (0)