Repository navigation
Add a user-token OAuth method to the Slack app #2368
Description
Source location
The public catalog serves the @executor.sh/slack package at https://catalog.executor.sh/apps/slack.json. Its published snapshot has five files and SHA-256 c138a9ce254fcb00b4cd6d901d64d230167a52b1d353b45da1c91b744e5fb071.
The catalog implementation identifies an executor-apps repository as the official feed producer. I could not establish its canonical contribution URL or path. Where should this provider change be submitted for the maintained @executor.sh/slack app?
Proposed change
The required generic host option is offered in #2371. Hosted operator client definitions are a separate concern in #2382.
The attached expanded-source patch adds a first user OAuth method. It sends user_scope only, with no OAuth scope parameter, requires authed_user.access_token on code exchange, and uses that token for health checks and the OpenAPI Bearer auth mapping. The existing manual-token and bot OAuth methods remain. Imported queries declare never() and mutations declare always(), including the existing GET mutation overrides. The default defineApp export, caller-owned accounts, framework fetch and decodeJson remain intact.
The comma-separated user_scope value is:
calls:read,calls:write,channels:history,channels:read,channels:write,chat:write,dnd:read,dnd:write,emoji:read,files:read,files:write,groups:history,groups:read,groups:write,im:history,im:read,im:write,links:write,mpim:history,mpim:read,mpim:write,pins:read,pins:write,reactions:read,reactions:write,reminders:read,reminders:write,remote_files:read,remote_files:share,search:read,team:read,usergroups:read,usergroups:write,users.profile:read,users.profile:write,users:read,users:read.email,users:write
Scope references in Slack's current method docs include conversation history, posting, reactions, files, search, Calls, Do Not Disturb, profiles, and remote files. The user grant excludes admin and identity scopes. Registration with these 38 scopes succeeded. Slack rejects tokens.basic with invalid_manifest and illegal_user_scopes, so it is omitted. Slack documents remote_files:write as bot-only; the stars.* methods are omitted because Slack says they no longer reflect end-user saved items and recommends retiring functionality that depends on them.
Expanded source patch
--- a/README.md
+++ b/README.md
@@ -8,12 +8,27 @@
**Auth**
-- Token: a bot token (`xoxb-`) or user token (`xoxp-`) from your Slack app's "OAuth & Permissions" page at https://api.slack.com/apps. User tokens are needed for methods such as `search.messages`.
-- OAuth: create a Slack app at https://api.slack.com/apps, add the redirect URI shown on Executor's connect page under "OAuth & Permissions", and enter its client ID and secret. The flow requests bot scopes (comma separated) and yields a bot token; reduce the scopes to what the app needs. Every requested scope was checked against its docs.slack.dev scope page as a bot scope; `mpim:write` is user-token only and is not requested.
+- Token: a bot token (`xoxb-`) or user token (`xoxp-`) from your Slack app's "OAuth & Permissions" page at https://api.slack.com/apps. This method keeps the supplied token as-is.
+- User OAuth (`user`): create a Slack app at https://api.slack.com/apps, add the redirect URI shown on the host's connect page, add the scopes below under **User Token Scopes**, and enter the client ID and secret. This flow sends a comma-separated `user_scope` only; it omits the OAuth `scope` parameter and does not request a bot grant. The exchange requires `authed_user.access_token`, which is used for health checks and API calls.
+ `user_scope` contains:
+
+ - Conversations: `channels:history`, `channels:read`, `channels:write`, `groups:history`, `groups:read`, `groups:write`, `im:history`, `im:read`, `im:write`, `mpim:history`, `mpim:read`, `mpim:write`.
+ - Messages and search: `chat:write`, `links:write`, `reactions:read`, `reactions:write`, `search:read`.
+ - Files: `files:read`, `files:write`, `remote_files:read`, `remote_files:share`.
+ - Calls and presence: `calls:read`, `calls:write`, `dnd:read`, `dnd:write`, `users:write`.
+ - Pins, reminders and workspace data: `emoji:read`, `pins:read`, `pins:write`, `reminders:read`, `reminders:write`, `team:read`, `usergroups:read`, `usergroups:write`, `users.profile:read`, `users.profile:write`, `users:read`, `users:read.email`.
+
+- Bot OAuth (`oauth`): use the existing bot-token flow with the redirect URI shown on the host's connect page, then enter the Slack app's client ID and secret. It requests the existing bot scopes and continues to use the top-level bot access token.
+
**Known gaps**
- No operations are skipped after the patches (171 of the 174 spec operations are exposed; the 3 removed are the oauth.* methods above).
-- OAuth yields a bot token only; user-token-only methods (search, some admin methods) need the token method.
+- The OpenAPI router exposes 171 methods. Admin methods still need admin scopes, and methods whose Slack documentation requires a bot or app-level token cannot use this user grant.
+- [`files.remote.add`](https://docs.slack.dev/reference/methods/files.remote.add/), [`files.remote.remove`](https://docs.slack.dev/reference/methods/files.remote.remove/), and [`files.remote.update`](https://docs.slack.dev/reference/methods/files.remote.update/) require `remote_files:write`. Slack documents that scope as bot-token-only and says remote files must be added by bots, not individual users. The user grant includes `remote_files:read` and `remote_files:share`, but not these writes.
+- Slack's [`stars.add`](https://docs.slack.dev/reference/methods/stars.add/), [`stars.list`](https://docs.slack.dev/reference/methods/stars.list/), and [`stars.remove`](https://docs.slack.dev/reference/methods/stars.remove/) methods still accept user scopes, but current docs say users can no longer view or interact with saved items through these APIs and recommend retiring app functionality that relies on them. The user grant does not request `stars:read` or `stars:write`.
+- Slack rejects `tokens.basic` as an illegal user scope during app registration. The user grant omits it.
+- [`users.identity`](https://docs.slack.dev/reference/methods/users.identity/) requires Slack identity scopes, which this grant intentionally omits.
+- The user grant omits `bookmarks:*` and `canvases:*` because the maintained OpenAPI description contains no corresponding operations.
- File uploads via the legacy multipart `files.upload` are not supported.
- Icon is Slack's hash mark from the Slack website (`a.slack-edge.com`), as Simple Icons has no Slack entry.
--- a/index.ts
+++ b/index.ts
@@ -11,11 +11,52 @@
string,
withApprovals,
} from "apps";
-import { always } from "apps/operations/approval";
+import { always, never } from "apps/operations/approval";
import { liveOpenapiRouter } from "apps/openapi";
import { icons } from "./icon.js";
import { options } from "./openapi.js";
+const userScopes = [
+ "calls:read",
+ "calls:write",
+ "channels:history",
+ "channels:read",
+ "channels:write",
+ "chat:write",
+ "dnd:read",
+ "dnd:write",
+ "emoji:read",
+ "files:read",
+ "files:write",
+ "groups:history",
+ "groups:read",
+ "groups:write",
+ "im:history",
+ "im:read",
+ "im:write",
+ "links:write",
+ "mpim:history",
+ "mpim:read",
+ "mpim:write",
+ "pins:read",
+ "pins:write",
+ "reactions:read",
+ "reactions:write",
+ "reminders:read",
+ "reminders:write",
+ "remote_files:read",
+ "remote_files:share",
+ "search:read",
+ "team:read",
+ "usergroups:read",
+ "usergroups:write",
+ "users.profile:read",
+ "users.profile:write",
+ "users:read",
+ "users:read.email",
+ "users:write",
+] as const;
+
// Slack answers HTTP 200 with {ok:false,error} for bad tokens, so decode `ok` first and only
// then rely on the fields that are present on success.
const Auth = object({
@@ -30,6 +71,16 @@
name: "Slack",
hosts: ["slack.com"],
auth: {
+ // Request only user scopes and require Slack's nested user grant on exchange.
+ user: oauth2({
+ authorizationUrl: "https://slack.com/oauth/v2/authorize",
+ tokenUrl: "https://slack.com/api/oauth.v2.access",
+ tokenEndpointAuthMethod: "client_secret_post",
+ scopeSeparator: ",",
+ scopes: [],
+ authorizationParams: { user_scope: userScopes.join(",") },
+ tokenResponse: { path: "authed_user", requiredOnExchange: true },
+ }),
token: secrets({
label: "Bot or user token (xoxb-/xoxp-)",
fields: object({ token: string({ minLength: 1 }) }),
@@ -75,7 +126,10 @@
}),
},
async health({ account, fetch, signal }) {
- const token = account.method === "oauth" ? account.fields.access_token : account.fields.token;
+ const token =
+ account.method === "user" || account.method === "oauth"
+ ? account.fields.access_token
+ : account.fields.token;
const response = await fetch("https://slack.com/api/auth.test", {
method: "POST",
signal,
@@ -97,7 +151,7 @@
user?.accounts.slack ?? [],
(account) =>
withApprovals(liveOpenapiRouter({ ...options, cache, fetch, signal, account }), (tool) =>
- tool.kind === "mutation" ? always() : undefined,
+ tool.kind === "mutation" ? always() : never(),
),
{ signal },
),
--- a/openapi.ts
+++ b/openapi.ts
@@ -40,6 +40,7 @@
allowedOrigin: "https://slack.com",
securitySchemes: { slackAuth: { type: "http", scheme: "bearer" } },
methods: {
+ user: [{ scheme: "slackAuth", field: "access_token", part: "value", prefix: "" }],
token: [{ scheme: "slackAuth", field: "token", part: "value", prefix: "" }],
oauth: [{ scheme: "slackAuth", field: "access_token", part: "value", prefix: "" }],
},Release prerequisite and verification
The offered patch retains the captured package's exact public apps@0.0.1-beta.100 dependency and "type": "module". That released type contract does not yet declare requiredOnExchange; strict typechecking fails with TS2769. This source is an offer for integration with the generic host change, not a publishable candidate on that release. After the host option ships, select the target host's exact public framework.release, update the pin and typecheck it before publication. The grant requirement must remain strict.
An earlier local mock run passed 169 checks against a carried runtime, including nested user-token selection, reads, approved posts and caller isolation. That result does not prove compatibility with a currently deployed public host. Live Slack acceptance remains open.
Activity